Przewodnik Web Crypto API: natywne szyfrowanie przeglądarki
Opanuj Web Crypto API do budowy szyfrowanych aplikacji transferowych. Kompletny przewodnik AES-GCM, RSA-OAEP i zarządzania kluczami.
Web Crypto API (zdefiniowane w rekomendacji W3C Web Cryptography API, dostępne jako window.crypto.subtle) to wbudowany w przeglądarkę sposób na wykonywanie kryptografii bez potrzeby ładowania dodatkowej biblioteki przez sieć. Obsługuje AES-GCM, AES-CBC, AES-CTR, AES-KW, HMAC, RSA-OAEP, RSA-PSS, RSASSA-PKCS1-v1_5, ECDH, ECDSA, HKDF i PBKDF2 we wszystkich nowoczesnych przeglądarkach (Chrome 37+, Firefox 34+, Safari 10.1+, Edge 79+). Dla aplikacji transferu plików oznacza to, że każdy bajt szyfrogramu może być generowany po stronie klienta przed wysłaniem, a przeglądarka dostarcza zaimplementowaną w stałym czasie, audytowaną implementację. Ten przewodnik omawia prymitywy kluczowe dla szyfrowanego transferu plików oraz typowe pułapki w pierwszych implementacjach.
SubtleCrypto jest oparty na Promise i jest asynchroniczny
Każda metoda crypto.subtle zwraca Promise. To celowe: operacje kryptograficzne mogą być przekazywane do sprzętu lub wątków w tle, więc wymuszenie asynchroniczności zapobiega blokowaniu głównego wątku. Przykładowy kod:
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true, // extractable
["encrypt", "decrypt"]
);
Drugi argument (true) oznacza klucz jako ekstrahowalny — może być później wyeksportowany przez crypto.subtle.exportKey(). Dla kluczy długoterminowych ustaw false, aby surowe bajty były nieosiągalne z poziomu JavaScript. Dla kluczy, które muszą być serializowane do fragmentu URL (wzorzec HexaTransfer), ustaw true.
Trzeci argument to tablica uprawnień klucza. Klucz wygenerowany z ["encrypt"] nie może być użyty do odszyfrowania, mimo że AES-GCM jest symetryczny. To oddzielenie zapobiega nadużywaniu skompromitowanego przepływu szyfrowania do odszyfrowania historycznych danych.
AES-GCM do symetrycznego szyfrowania plików
AES-GCM to podstawowy algorytm do szyfrowania zawartości pliku. Zapewnia uwierzytelnione szyfrowanie z powiązanymi danymi (AEAD): szyfrogram plus tag uwierzytelniający plus opcjonalne powiązane dane, które są uwierzytelniane, ale nie szyfrowane. Do transferu pliku użyj 256-bitowego klucza i 96-bitowego nonce zgodnie z zaleceniami NIST SP 800-38D.
const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-bitowy nonce
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
plaintext
);
Wynik zawiera 128-bitowy tag uwierzytelniający GCM dołączony do szyfrogramu. Deszyfrowanie automatycznie weryfikuje tag i zgłasza błąd w razie niezgodności. Nigdy nie używaj ponownie nonce z tym samym kluczem — bezpieczeństwo GCM całkowicie się załamuje przy powtórzeniu nonce (atakujący może odzyskać klucz uwierzytelniający). W transferze pliku, gdzie każdy plik dostaje świeży klucz, losowe nonce są bezpieczne; dla kluczy długoterminowych użyj licznika.
PBKDF2 do kluczy wyprowadzanych z hasła
Gdy użytkownicy wpisują hasło do ochrony pliku, nie można użyć hasła bezpośrednio jako klucza AES. Należy je najpierw przetworzyć przez PBKDF2:
const passwordKey = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2",
false,
["deriveKey"]
);
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"]
);
Wytyczne OWASP dotyczące hashowania haseł z 2023 roku zalecają 600 000 iteracji dla PBKDF2-SHA-256. Cokolwiek poniżej 310 000 nie spełnia obecnych najlepszych praktyk. Sól musi być losowa i przechowywana wraz z szyfrogramem (nie jest sekretem, musi być tylko unikalna).
W nowym kodzie w 2026 roku rozważ Argon2id zamiast PBKDF2. Argon2 nie jest jeszcze dostępny w Web Crypto API, ale biblioteki takie jak argon2-browser lub @noble/hashes dostarczają implementacje JavaScript/WASM. Argon2id znacznie lepiej opiera się atakom GPU niż PBKDF2.
RSA-OAEP do zawijania kluczy
W scenariuszach, gdzie chcesz zaszyfrować klucz AES pliku kluczem publicznym odbiorcy, użyj RSA-OAEP. Generowanie kluczy:
const keyPair = await crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]), // 65537
hash: "SHA-256",
},
true,
["encrypt", "decrypt"]
);
Dla nowych kluczy używaj modulusLength 4096; 2048 jest akceptowalne, ale będzie stopniowo wycofywane w miarę jak harmonogramy kwantowe się precyzują. RSA-OAEP szyfruje tylko małe ładunki (co najwyżej modulusLength/8 - 2*hashLength - 2 bajtów), więc zawijaj 256-bitowy klucz AES, a nie szyfruj zawartości pliku bezpośrednio.
W aplikacjach wymagających wydajności, ECDH z P-256 lub P-384 to lepsza alternatywa niż RSA. Generowanie kluczy jest rząd wielkości szybsze, a rozmiary kluczy znacznie mniejsze.
Przesyłanie strumieniowe dla dużych plików
Plik 2 GB nie mieści się wygodnie w ArrayBuffer przeglądarki. Chrome, Firefox i Safari pozwalają na odczyt plików przez File.stream() zwracający ReadableStream, a następnie przetwarzanie fragmentami. Web Crypto API samo w sobie nie ma jeszcze metod szyfrowania/deszyfrowania strumieniowego (to luka w specyfikacji), więc stosuje się dwa obejścia:
- Podziel na fragmenty (64 KB lub 1 MB) i zaszyfruj każdy unikalnym nonce. Odbiorca łączy je w kolejności. Traci się prawdziwe AEAD dla całego pliku, ale działa w większości przypadków.
- Użyj biblioteki WASM (libsodium.js, @noble/ciphers z backendem WASM), która obsługuje strumieniowe tryby AEAD jak XChaCha20-Poly1305 lub AES-GCM-SIV.
Dla transferów poniżej kilkuset megabajtów buforowane AES-GCM działa dobrze i jest znacznie prostsze. Powyżej tej granicy strumieniowanie staje się konieczne, by uniknąć nadmiernego zużycia pamięci.
Eksport, import kluczy i fragmenty URL
W przepływach w stylu HexaTransfer, gdzie klucz podróżuje we fragmencie URL:
const rawKey = await crypto.subtle.exportKey("raw", aesKey);
const keyBase64 = btoa(String.fromCharCode(...new Uint8Array(rawKey)));
// URL udostępnienia jak https://example.com/file/abc123#key=keyBase64
Fragmenty URL nigdy nie są przesyłane na serwery w żądaniach HTTP (przeglądarka je usuwa). Dzięki temu klucz pozostaje po stronie klienta, nawet gdy użytkownik udostępnia link. Po stronie odbiorcy:
const keyBase64 = window.location.hash.slice(5); // usuń "#key="
const rawKey = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
const key = await crypto.subtle.importKey(
"raw", rawKey, "AES-GCM", false, ["decrypt"]
);
Użyj kodowania base64url (zamień + na -, / na _, usuń dopełnienie), aby uniknąć problemów z kodowaniem URL.
Typowe pułapki
Używanie Math.random() do soli lub IV. Math.random() nie jest kryptograficznie bezpieczny. Zawsze używaj crypto.getRandomValues().
Ponowne użycie IV z tym samym kluczem. Właściwości bezpieczeństwa GCM całkowicie się załamują przy powtórzeniu nonce. Losowe 96-bitowe nonce kolidują po ~2^48 szyfrowaniach pod tym samym kluczem (granica urodzinowa). W transferze pliku, gdzie każdy plik ma własny klucz — bezpieczne; dla kluczy długoterminowych użyj licznika.
Zapomnienie o HTTPS. crypto.subtle jest dostępne tylko w bezpiecznych kontekstach (HTTPS lub localhost). W niezabezpieczonym źródle crypto.subtle jest niezdefiniowane.
Przechowywanie ekstrahowalnych kluczy w IndexedDB bez ochrony. Jeśli musisz utrwalić klucze, zawijaj je (np. kluczem wyprowadzonym z hasła) przed zapisem. Nigdy nie przechowuj surowych kluczy AES w localStorage, dostępnym dla każdego skryptu w domenie.
Ufanie hasłom użytkownika bez PBKDF2. Surowe hasło przekonwertowane na bajty UTF-8 to nie 256-bitowy klucz. Zawsze wyprowadzaj klucz.
Niepomijanie weryfikacji tagów uwierzytelniających. crypto.subtle.decrypt() robi to automatycznie dla AES-GCM, ale jeśli implementujesz niestandardowe protokoły, nie pomijaj tej weryfikacji.
Niuanse obsługi przeglądarek
Wszystkie główne przeglądarki obsługują Web Crypto przez HTTPS. Kilka specyficznych zachowań:
- PBKDF2 w Safari był wolniejszy niż w Chrome/Firefox przez lata; różnica zniknęła w Safari 15.
- Firefox wymusza ściślejszą walidację danych wejściowych; kod działający w Chrome może zgłaszać
OperationErrorw Firefox. Testuj w obu. - Web Crypto w service workers działa, ale wymaga, by zakres rejestracji był HTTPS.
- Node.js od wersji 15 udostępnia
require("crypto").webcryptoz kompatybilnym API, przydatne do izomorficznego kodu kryptograficznego.
Kiedy użyć biblioteki zamiast natywnego API
Web Crypto dobrze pokrywa podstawy, ale brakuje mu nowoczesnych prymitywów jak ChaCha20-Poly1305, Argon2, X25519 i Ed25519 (choć Ed25519 jest wprowadzane). Dla tych potrzeb libsodium.js (przez WASM) lub @noble/ciphers / @noble/curves (czysty JavaScript, audytowane) to wiodące opcje. HexaTransfer używa prymitywów Web Crypto bezpośrednio dla AES-GCM i PBKDF2, ponieważ pokrywają one ścieżkę transferu pliku bez zależności.
Dla pełnego przepływu szyfrowanego transferu Web Crypto wystarczy w mniej niż 100 liniach kodu: wygeneruj klucz AES, zaszyfruj plik, prześlij szyfrogram, udostępnij link z kluczem we fragmencie, odbiorca importuje klucz i odszyfrowuje. To cała procedura.
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