Autentifikované šifrovanie
Aead\Aes256Gcm implementuje AEAD_AES_256_GCM (RFC 5116 §5.2, NIST SP 800-38D) nad ext-openssl: 32-bajtový kľúč, 12-bajtový nonce a 16-bajtový autentifikačný tag pripojený za šifrový text. Šifrovanie je tu autentifikované — open() a decrypt() vrátia otvorený text len vtedy, keď tag dokáže, že šifrový text, nonce aj pridružené dáta sú presne tie, ktoré sa pod daným kľúčom zapečatili. Čokoľvek iné vyhodí výnimku a nič sa nedešifruje:
use RoundlyConsulting\Crypto\Aead\Aes256Gcm;
use RoundlyConsulting\Crypto\Aead\DecryptionFailedException;
use RoundlyConsulting\Crypto\Facades\Crypto;
// A key is 32 random bytes. Keep it like any other secret, never next to the data.
$key = Crypto::randomBytes(Aes256Gcm::KEY_BYTES);
$aead = Crypto::aes256Gcm(); // or new Aes256Gcm
// A fresh random nonce, carried in front: nonce ‖ ciphertext ‖ tag.
$sealed = $aead->seal($key, $plaintext, associatedData: 'invoices:42');
try {
$plaintext = $aead->open($key, $sealed, associatedData: 'invoices:42');
} catch (DecryptionFailedException) {
// wrong key or context, or a changed ciphertext — one message for every cause
}Pridružené dáta (associated data) sa autentifikujú, no nešifrujú — vložte do nich všetko, k čomu má šifrový text zostať viazaný (účel, identitu záznamu), a šifrový text skopírovaný kamkoľvek inam sa neotvorí. Trieda je final readonly, nemá argumenty konštruktora a nedrží žiadny kľúčový materiál: každý kľúč sa odovzdáva pri volaní a je označený #[\SensitiveParameter]. Fasáda, injektovaný manažér aj samotná trieda spúšťajú rovnaký kód; Crypto::aes256Gcm() zakaždým vráti novú inštanciu — je bezstavová, takže jej ukladanie nič neprinesie:
use RoundlyConsulting\Crypto\Aead\Aes256Gcm;
use RoundlyConsulting\Crypto\CryptoManager;
use RoundlyConsulting\Crypto\Facades\Crypto;
Crypto::aes256Gcm()->seal($key, $plaintext, $aad); // facade
app(CryptoManager::class)->aes256Gcm()->seal($key, $plaintext, $aad); // injected manager
(new Aes256Gcm)->seal($key, $plaintext, $aad); // the class itselfKedy ho použiť
| Potrebujete | Použite |
|---|---|
| Zašifrovať hodnotu kľúčom aplikácie bez ničoho, k čomu by ju bolo treba viazať | Crypt / encrypt() z Laravelu — používa APP_KEY, rotuje cez APP_PREVIOUS_KEYS a nemá vstup pre pridružené dáta. |
| Šifrový text, ktorý sa smie otvoriť len vo svojom kontexte — tento záznam, tento tenant, tento účel | Aes256Gcm s pridruženými dátami. |
| Iný kľúč než APP_KEY — jeden kľúč na účel alebo kľúč, ktorý drží iná služba | Aes256Gcm. |
| Kompaktný binárny formát, ktorý prečíta každá implementácia RFC 5116 / AES-256-GCM | Aes256Gcm::encrypt() alebo seal(). |
| Dokázať pravosť správy, ktorá zostáva čitateľná — webhooky, podpísané URL | Hash\Hmac, nie šifrovanie — pozrite HMAC a hašovanie. |
| Hodnotu na vyhľadávanie, ktorú nikdy netreba dešifrovať | Hash\Digest. |
Veľkosti a metódy
| Konštanta | Hodnota | Význam |
|---|---|---|
Aes256Gcm::KEY_BYTES | 32 | Dĺžka kľúča (AES-256). |
Aes256Gcm::NONCE_BYTES | 12 | Dĺžka nonce (96 bitov, predvolená pre GCM). |
Aes256Gcm::TAG_BYTES | 16 | Dĺžka tagu (128 bitov, pripojený za šifrový text). |
Iné veľkosti trieda nepodporuje — žiadne AES-128 ani AES-192, žiadne kratšie tagy ani iné dĺžky nonce. Kľúč alebo nonce inej dĺžky vyhodí InvalidAeadParameterException.
| Metóda | Vracia |
|---|---|
seal(string $key, string $plaintext, string $associatedData = '') | nonce ‖ šifrový text ‖ tag pod novým náhodným nonce. |
open(string $key, string $sealed, string $associatedData = '') | Otvorený text výstupu zo seal(). |
encrypt(string $key, string $nonce, string $plaintext, string $associatedData = '') | šifrový text ‖ tag — nonce nie je súčasťou. |
decrypt(string $key, string $nonce, string $ciphertext, string $associatedData = '') | Otvorený text výstupu z encrypt(). |
- seal() a open() sú dvojica na bežné použitie: nonce sa vygeneruje a prenesie za vás.
- encrypt() a decrypt() sú rozhranie podľa RFC 5116: nonce dodávate a ukladáte sami.
- seal() vráti NONCE_BYTES + strlen($plaintext) + TAG_BYTES bajtov (réžia 28 bajtov); encrypt() vráti strlen($plaintext) + TAG_BYTES. Prázdny otvorený text je povolený a zapečatí sa do 28 bajtov.
- GCM nepoužíva zarovnanie (padding), takže šifrový text je rovnako dlhý ako otvorený text — šifrovanie skryje obsah, nie dĺžku.
- Dve zapečatenia toho istého textu pod tým istým kľúčom sa líšia, pretože každé použije nový nonce.
- Celá správa sa spracúva v pamäti; streamovacie API neexistuje.
Na vstupe aj na výstupe surové bajty
Kľúče, nonce aj šifrové texty sú binárne reťazce. Pred uložením do textového stĺpca (alebo do JSON či URL) ich zakódujte a pred otvorením dekódujte. Binárny stĺpec ($table->binary()) ich prijme tak, ako sú:
$stored = Crypto::base64Encode($aead->seal($key, $plaintext, associatedData: 'invoices:42'));
$plaintext = $aead->open($key, Crypto::base64Decode($stored), associatedData: 'invoices:42');Pridružené dáta viažu šifrový text k jeho kontextu
Pridružené dáta sa neukladajú — výstup ich nijako nenesie. Do seal() aj open() odovzdajte rovnaké bajty: pri otváraní ich znova zostavte z kontextu (vyššie invoices:42 z práve čítanej faktúry) a držte ich formát stabilný — iný reťazec sa neotvorí, ani keď znamená to isté. Použite ich na všetko, od čoho sa šifrový text nesmie nikdy oddeliť: účel, záznam, ku ktorému patrí (tabuľka, primárny kľúč, stĺpec), tenant či vlastník, verzia formátu alebo schémy:
$aadFor = fn (int $id): string => implode('|', ['invoices', $id, 'iban']);
$sealed = $aead->seal($key, $iban, $aadFor(42));
$aead->open($key, $sealed, $aadFor(42)); // the IBAN
// The same bytes moved to invoice 43's row don't decrypt there. This throws
// DecryptionFailedException:
$aead->open($key, $sealed, $aadFor(43));Bez pridružených dát by útočník s právom zápisu do databázy mohol vymeniť šifrové texty dvoch riadkov a aplikácia by každý z nich dešifrovala na nesprávnom mieste. S nimi sa vymenený text neotvorí.
- Predvolená hodnota je prázdny reťazec '' (bez kontextu); open() potom musí tiež dostať ''.
- Pridružené dáta nie sú tajné — nevkladajte do nich nič, čo by ste neuložili ako otvorený text.
- Zostavte ich zo stabilných hodnôt, ktoré viete pri dešifrovaní znova vypočítať — z primárneho kľúča, nie z meniacej sa časovej pečiatky.
- Časti spájajte oddeľovačom, ktorý sa vo vnútri časti nemôže vyskytnúť, aby ['a|b', 'c'] a ['a', 'b|c'] nikdy nedali rovnaký reťazec.
Kľúče
Kľúč je presne 32 náhodných bajtov — nikdy nie heslo ani iný reťazec zvolený človekom. Balík nepotrebuje konfiguráciu, preto sám nikdy kľúč nenačíta: kľúč načíta váš kód a odovzdá ho:
use RoundlyConsulting\Crypto\Aead\Aes256Gcm;
use RoundlyConsulting\Crypto\Facades\Crypto;
// Generate once, e.g. in a tinker session or a setup command
$key = Crypto::randomBytes(Aes256Gcm::KEY_BYTES);
echo Crypto::base64UrlEncode($key); // put this in your secret store / .env
// Load at runtime, from your own config key
$key = Crypto::base64UrlDecode(config('services.vault.key'));- Jeden kľúč na účel. Šifrovací kľúč nezdieľajte s podpisovaním, HMAC ani inou funkciou.
- Rotácia. Trieda prijíma jeden kľúč na volanie a nemá zväzok kľúčov. Pri rotácii si ponechajte starý kľúč, aby sa existujúce hodnoty dali otvoriť cez open(), nové hodnoty pečaťte cez seal() novým kľúčom a zaznamenajte, ktorý kľúč ktorú hodnotu zapečatil — napríklad v stĺpci s id kľúča vedľa šifrového textu.
Ak potrebujete viac kľúčov z jedného hlavného tajomstva, poslúži natívna funkcia PHP hash_hkdf():
$invoiceKey = hash_hkdf('sha256', $masterKey, Aes256Gcm::KEY_BYTES, 'invoices');
$tokenKey = hash_hkdf('sha256', $masterKey, Aes256Gcm::KEY_BYTES, 'api-tokens');Nonce
- seal() sa o nonce postará: vytiahne nový 96-bitový nonce z CSPRNG (Random\Bytes), vloží ho na začiatok výstupu a open() ho odtiaľ prečíta. Vy ho nikdy neuvidíte.
- Nonce nikdy nepoužite pod tým istým kľúčom dvakrát. Pri GCM dve správy pod jedným kľúčom a jedným nonce prezradia XOR oboch otvorených textov a umožnia útočníkovi falšovať tagy — práve preto existuje seal().
- Pri náhodných nonce udržte počet zapečatení jedným kľúčom pod 2³² (NIST SP 800-38D §8.3); skôr, než k tomu dôjde, kľúč vymeňte alebo odvoďte kľúče pre jednotlivé účely.
encrypt() a decrypt() prijímajú váš nonce. Použite ich len vtedy, keď nonce určuje protokol alebo iný systém, prípadne ho ukladá oddelene — jedinečnosť je potom na vás:
use RoundlyConsulting\Crypto\Aead\Aes256Gcm;
use RoundlyConsulting\Crypto\Facades\Crypto;
$aead = Crypto::aes256Gcm();
$nonce = Crypto::randomBytes(Aes256Gcm::NONCE_BYTES); // or a counter you never repeat
$ciphertext = $aead->encrypt($key, $nonce, $plaintext, $associatedData); // ciphertext ‖ tag
$plaintext = $aead->decrypt($key, $nonce, $ciphertext, $associatedData);seal() je presne $nonce . encrypt($key, $nonce, …), takže open($key, $nonce.$ciphertext, $aad) otvorí aj výstup z encrypt().
Interoperabilita
encrypt() vytvára štandardné AES-256-GCM — šifrový text, za ktorým nasleduje 128-bitový tag, s autentifikovanými pridruženými dátami. Prečíta ho každá vyhovujúca implementácia, ktorá má kľúč, nonce a pridružené dáta: openssl_decrypt() priamo v PHP, crypto v Node, crypto/cipher v Go, cryptography v Pythone aj Web Crypto API (ktoré tiež očakáva šifrový text ‖ tag). Výstup zo seal() je to isté s 12-bajtovým nonce na začiatku:
// The tag is the last 16 bytes; native OpenSSL opens it.
$plaintext = openssl_decrypt(
substr($ciphertext, 0, -16), 'aes-256-gcm', $key, OPENSSL_RAW_DATA,
$nonce, substr($ciphertext, -16), $associatedData,
);Chyby
| Výnimka | Vyhadzuje sa, keď | Význam |
|---|---|---|
Aead\DecryptionFailedException | open() alebo decrypt() nedokáže vstup autentifikovať: nesprávny kľúč, nonce či pridružené dáta, zmenený šifrový text alebo tag, prípadne vstup príliš krátky na tag (decrypt(): pod 16 bajtov; open(): pod 28 bajtov). | Problém s dátami — hodnotu považujte za cudziu alebo pozmenenú. |
Aead\InvalidAeadParameterException | Kľúč nemá 32 bajtov (keyLength()), nonce nemá 12 bajtov (nonceLength()) alebo OpenSSL šifru odmietne (cipherUnavailable(), na funkčnom OpenSSL nedosiahnuteľné). | Programátorská chyba, nikdy nie vlastnosť dát. |
- DecryptionFailedException nesie vždy rovnakú správu — The ciphertext could not be authenticated; nothing was decrypted. — bez ohľadu na príčinu. Nikdy nepovie, ktorá časť zlyhala, a nikdy neobsahuje kľúč ani otvorený text.
- InvalidAeadParameterException uvádza len dĺžky, napríklad An AES-256-GCM key must be 32 bytes, [16] given.
- open() kontroluje dĺžku zapečateného vstupu skôr než kľúč: čokoľvek kratšie ako 28 bajtov (NONCE_BYTES + TAG_BYTES) vyhodí DecryptionFailedException, aj keď má nesprávnu dĺžku aj kľúč.
- Obe dedia z CryptoException — pozrite Výnimky.
Chybu dát zachyťte, programátorskú chybu nechajte prebublať:
use RoundlyConsulting\Crypto\Aead\DecryptionFailedException;
use RoundlyConsulting\Crypto\Aead\InvalidAeadParameterException;
try {
$secret = $aead->open($key, $sealed, $aad);
} catch (DecryptionFailedException) {
abort(404); // not yours / tampered: don't explain why
}
// InvalidAeadParameterException is left to bubble up: it is a bug in the caller.Ako je to overené
Implementáciu pripínajú známe testovacie vektory AES-256 zo špecifikácie GCM (McGrew & Viega, príloha B, testovacie prípady 13–16, overené v oboch smeroch) a každý prípad AES-256 / 96-bitový nonce / 128-bitový tag z korpusu AES-GCM projektu Wycheproof — 39 platných prípadov (presný šifrový text aj tag v oboch smeroch) a 27 neplatných, z ktorých každý musí vyhodiť DecryptionFailedException.
Prejavte lásku k open source
Tento balík je zadarmo pod licenciou MIT. Ak vám šetrí čas, jednorazový príspevok alebo členstvo na Patreone nám pomôže ho ďalej udržiavať, testovať a dokumentovať.
Ďalšie spôsoby podpory vrátane kryptomienOdoslaním daru súhlasíte s našimi podmienkami prijímania darov.
Chcete to zabudovať do svojho produktu?
Naše balíky integrujeme do zákazkových Laravel a AI riešení. Napíšte nám, na čom pracujete, a ozveme sa do 48 hodín.