Przewodnik implementacji AES-GCM: uwierzytelnione szyfrowanie
Zaimplementuj poprawnie szyfrowanie AES-GCM. Zarządzanie nonce, kluczami i typowe pułapki do uniknięcia.
AES-GCM (Galois/Counter Mode) łączy szyfrowanie AES-CTR z uwierzytelnianiem GHASH, tworząc uwierzytelnione szyfrowanie z powiązanymi danymi (AEAD). Poprawna implementacja używa 256-bitowego klucza, 96-bitowego (12-bajtowego) nonce unikalnego na klucz (nigdy niepowtarzanego), 128-bitowego tagu uwierzytelniającego i opcjonalnych uwierzytelnionych danych powiązanych (AAD), które są weryfikowane, ale nie szyfrowane. NIST SP 800-38D określa dokładną konstrukcję. Błąd w którymkolwiek z tych elementów — szczególnie powtórzenie nonce — prowadzi do całkowitego załamania bezpieczeństwa GCM: jedna powtórzona para (klucz, nonce) pozwala atakującemu odzyskać klucz uwierzytelniający i fałszować dowolne szyfrogramy. Ten przewodnik obejmuje poprawny sposób korzystania z AES-GCM w przeglądarkach, Node i kontekstach serwerowych.
Co faktycznie gwarantuje GCM
Dwie właściwości:
Poufność: treść w jawnym tekście nie może zostać odtworzona bez klucza. Warstwa szyfrowania w trybie CTR AES-GCM to zapewnia.
Integralność i autentyczność: każda modyfikacja szyfrogramu, nonce lub danych powiązanych powoduje niepowodzenie deszyfrowania. GHASH produkuje 128-bitowy tag weryfikowany w stałym czasie przy deszyfrowaniu.
Czego GCM nie gwarantuje: niezaprzeczalności (jest symetryczny, więc każdy posiadający klucz może tworzyć prawidłowe szyfrogramy), ochrony przed powtórzeniem (to sprawa wyższej warstwy) ani kolejności (dla strumieni musisz zapewnić jakiś łańcuch).
Kluczowy wniosek: GCM pozostaje bezpieczny wyłącznie gdy nonce są unikalne na klucz. Nie prawie unikalne, nie zazwyczaj unikalne — faktycznie unikalne. Dowód bezpieczeństwa upada przy ponownym użyciu.
Zarządzanie nonce: najważniejsza kwestia
96-bitowy nonce można generować na dwa sposoby:
Losowo: crypto.getRandomValues(new Uint8Array(12)). Przy losowych 96-bitowych nonce pod jednym kluczem kolizje według granicy urodzinowej pojawiają się około 2^48 szyfrowań. NIST sugeruje margines bezpieczeństwa, więc ogranicz użycia do 2^32 na klucz.
Licznik: inkrementuj 96-bitową liczbę całkowitą. Gwarantuje unikalność do 2^96 wiadomości. Wymaga niezawodnego stanu monotonicznego, trudnego w systemach rozproszonych.
Przy transferze pliku ze świeżym kluczem na plik, losowe nonce są całkowicie bezpieczne — nigdy nie dotrzesz do 2^32 szyfrowań jednym kluczem. Do szyfrowania fragmentowanego pod jednym kluczem pliku używaj licznika, gdzie nonce koduje indeks fragmentu:
const nonce = new Uint8Array(12);
new DataView(nonce.buffer).setUint32(0, messageId);
new DataView(nonce.buffer).setBigUint64(4, BigInt(chunkIndex));
Scenariusz katastrofy: wiele procesów szyfrujących pod tym samym wspólnym kluczem z losowymi nonce, skalujących się do milionów szyfrowania na sekundę. Kolizje urodzinowe stają się prawdopodobne. Jeśli musisz współdzielić klucze między procesami, użyj skoordynowanego licznika z prefiksem ID procesu.
Nie używaj 64-bitowych nonce
AES-GCM obsługuje zmienne długości nonce, ale tylko 96-bitowe nonce używają zoptymalizowanej konstrukcji określonej w NIST 800-38D. Inne długości (zwykle 64 lub 128 bitów) uruchamiają krok wstępnego przetwarzania GHASH, który zmniejsza wydajność i zwiększa złożoność. Web Crypto API akceptuje IV inne niż 96-bitowe, ale specyfikacja zaleca 96. Po prostu używaj 96.
Długość tagu: nie skracaj
Tag GCM ma do 128 bitów. Niektóre specyfikacje dopuszczają skrócenie do 96, 64 lub nawet 32 bitów. Nie rób tego. Skrócone tagi ułatwiają ataki fałszowania, a oszczędności (4–12 bajtów na wiadomość) są nieistotne przy transferze pliku. AES-GCM Web Crypto domyślnie używa 128-bitowych tagów przez parametr tagLength (domyślnie 128). Zostaw to bez zmian.
Dane powiązane (AAD)
AAD to dane, które są uwierzytelniane, ale nie szyfrowane. Używaj ich do metadanych, które chcesz powiązać z szyfrogramem: nazwa pliku, typ zawartości, znacznik czasu wygaśnięcia, ID przesyłającego.
await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: nonce,
additionalData: new TextEncoder().encode(JSON.stringify({
filename: "raport.pdf",
contentType: "application/pdf",
expires: 1712345678,
})),
},
key,
plaintext
);
Jeśli atakujący zmodyfikuje AAD, deszyfrowanie nie powiedzie się. Zapobiega to atakom zamiany, gdzie ktoś podmienia nazwę pliku na przechowywanym szyfrogramie bez wykrycia. Odbiorca musi znać dokładne AAD do odszyfrowania, więc przechowuj je razem z szyfrogramem.
Generowanie i wyprowadzanie kluczy
Dla kluczy per plik:
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
256 bitów to standard w 2026 roku. 128-bitowy AES jest nadal bezpieczny, ale ma mniejszy margines post-kwantowy (algorytm Grovera zmniejsza efektywną wytrzymałość o połowę).
Dla kluczy wyprowadzanych z hasła:
const aesKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: crypto.getRandomValues(new Uint8Array(16)),
iterations: 600000,
hash: "SHA-256",
},
passwordKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
Przechowuj sól razem z szyfrogramem. Nie jest sekretem; musi być tylko unikalna na hasło.
Krytyczna ścieżka kodu
Minimalna funkcja szyfrowania:
async function encrypt(key, plaintext, aad = new Uint8Array()) {
const nonce = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = new Uint8Array(
await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: nonce, additionalData: aad },
key,
plaintext
)
);
return { nonce, ciphertext, aad };
}
Deszyfrowanie z właściwą obsługą błędów:
async function decrypt(key, { nonce, ciphertext, aad }) {
try {
return await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: nonce, additionalData: aad },
key,
ciphertext
);
} catch (e) {
// Błąd uwierzytelniania
throw new Error("Deszyfrowanie nieudane: szyfrogram zmodyfikowany lub zły klucz");
}
}
Wywołanie decrypt zgłasza OperationError przy niezgodności tagu, zbyt krótkim szyfrogramie lub złym kluczu. Traktuj każdy wyjątek jako błąd integralności; nie próbuj rozróżniać.
Fragmentowanie dużych plików
Dla plików powyżej kilkuset megabajtów fragmentuj je, by uniknąć presji pamięci:
async function encryptChunks(key, file, chunkSize = 1024 * 1024) {
const chunks = [];
let chunkIndex = 0;
for (let offset = 0; offset < file.size; offset += chunkSize) {
const chunk = await file.slice(offset, offset + chunkSize).arrayBuffer();
const nonce = new Uint8Array(12);
new DataView(nonce.buffer).setBigUint64(4, BigInt(chunkIndex++));
const ct = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: nonce }, key, chunk
);
chunks.push(new Uint8Array(ct));
}
return chunks;
}
Zastrzeżenie: fragmentowany AES-GCM nie wykrywa obcięcia. Atakujący mógłby upuścić końcowe fragmenty, a każdy przeżywający fragment odszyfrowuje się poprawnie. Dla ochrony uwzględnij łączną liczbę fragmentów w AAD każdego fragmentu lub użyj crypto_secretstream libsodium, który to obsługuje.
Deszyfrowanie po stronie serwera (Node.js)
Moduł crypto Node może odszyfrować dane zaszyfrowane w przeglądarce:
const { createDecipheriv } = require('crypto');
function decrypt(key, nonce, ciphertextWithTag) {
const tag = ciphertextWithTag.slice(-16);
const ct = ciphertextWithTag.slice(0, -16);
const decipher = createDecipheriv('aes-256-gcm', key, nonce);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(ct), decipher.final()]);
}
Web Crypto dołącza 128-bitowy tag do szyfrogramu; API Node oczekuje tagu i szyfrogramu oddzielnie. Rozdzielaj odpowiednio.
Liczby wydajności
Na typowym sprzęcie z 2024–2026 z AES-NI:
- Natywny (OpenSSL, AES-NI): 3–5 GB/s na rdzeń
- Web Crypto (przeglądarka z akceleracją sprzętową): 1–2 GB/s
- libsodium.js WASM AES-GCM: 400–800 MB/s
- Czysty JS (@noble/ciphers): 50–150 MB/s
Dla pliku 1 GB szyfrowanie Web Crypto działa w 0,5–1 sekundy. Czysty JS zajmuje 7–20 sekund. Dobieraj implementacje w oparciu o tę rzeczywistość; dla UX transferu dużych plików Web Crypto to praktyczny wybór.
Podsumowanie typowych pułapek
- Ponowne użycie nonce: katastrofalne. Największy pojedynczy tryb awarii.
- Używanie
Math.random()zamiastcrypto.getRandomValues(). - Zapomnienie o uwierzytelnieniu powiązanych metadanych przez AAD.
- Używanie trybu CBC „bo jesteśmy do niego przyzwyczajeni". CBC wymaga oddzielnego MAC by dorównać integralności GCM; konstrukcja HMAC-CBC jest poprawna, ale trudna do właściwego wdrożenia, GCM eliminuje tę pułapkę.
- Ciche łapanie błędów deszyfrowania i zwracanie śmieci. Zawsze fail głośno.
- Implementowanie własnego GCM. Używaj Web Crypto, libsodium lub node:crypto. Implementacja GHASH ma pułapki side-channel, na które eksperci pracowali latami.
HexaTransfer używa AES-256-GCM Web Crypto z 96-bitowymi losowymi nonce, 128-bitowymi tagami i bez AAD, bo klucz jest per plik, a nazwa pliku jest przechowywana osobno w metadanych chronionych AEAD. Proste, poprawne, szybkie.
Kiedy wybrać coś innego
AES-GCM jest optymalny do transferu pliku, ale rozważ alternatywy w konkretnych przypadkach:
- XChaCha20-Poly1305: 192-bitowe nonce sprawiają, że bezpieczeństwo losowych nonce jest trywialne w każdej skali. Nieco wolniejszy na sprzęcie z AES-NI, szybszy na starszym ARM bez AES-NI. Dostępny przez libsodium.
- AES-GCM-SIV: odporny na błędne użycie; ponowne użycie nonce nie wycieka klucza, ujawnia tylko czy jawne teksty były równe. Przydatny gdy nie możesz zagwarantować unikalności nonce.
Dla większości obciążeń transferu pliku w standardowym stosie webowym, AES-256-GCM ze świeżym kluczem na plik i losowymi 96-bitowymi nonce to właściwy i najprostszy do poprawnej implementacji wybór.
Wypróbuj na hexatransfer.com — bezpłatnie, bez konta, do 10 GB.
Wysyłaj duże pliki bezpiecznie z szyfrowaniem end-to-end
Przesyłaj pliki do 10 GB za darmo z szyfrowaniem end-to-end. Bez rejestracji. Twoje pliki są szyfrowane w przeglądarce przed przesłaniem — nikt inny nie może ich odczytać.
Wyślij plik