NovinkaZverejnili sme 50+ Laravel balíkov ako open source
Custom AI apps, agents and automation — Roundly ConsultingRoundly
Všetky balíky
Crypto for Laravel

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 itself

Kedy ho použiť

PotrebujetePouž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 účelAes256Gcm s pridruženými dátami.
Iný kľúč než APP_KEY — jeden kľúč na účel alebo kľúč, ktorý drží iná službaAes256Gcm.
Kompaktný binárny formát, ktorý prečíta každá implementácia RFC 5116 / AES-256-GCMAes256Gcm::encrypt() alebo seal().
Dokázať pravosť správy, ktorá zostáva čitateľná — webhooky, podpísané URLHash\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štantaHodnotaVýznam
Aes256Gcm::KEY_BYTES32Dĺžka kľúča (AES-256).
Aes256Gcm::NONCE_BYTES12Dĺžka nonce (96 bitov, predvolená pre GCM).
Aes256Gcm::TAG_BYTES16Dĺž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ódaVracia
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ýnimkaVyhadzuje sa, keďVýznam
Aead\DecryptionFailedExceptionopen() 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\InvalidAeadParameterExceptionKľúč 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 kryptomien

Odoslaní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.