Web Crypto API: kompletny tutorial szyfrowania plików
Opanuj Web Crypto API do szyfrowania plików w browserze. AES-GCM, RSA-OAEP oraz zarządzanie kluczami w aplikacjach JavaScript.
UODO wielokrotnie podkreślało, że szyfrowanie danych przed ich transferem przez internet to jeden z kluczowych środków technicznych wymaganych przez RODO (art. 32). Web Crypto API pozwala szyfrować pliki bezpośrednio w przeglądarce za pomocą natywnych metod window.crypto.subtle — bez żadnych zewnętrznych bibliotek. Do szyfrowania plików standardowo wyprowadza się klucz z hasła przez PBKDF2 (210 000 iteracji, SHA-256), a następnie szyfruje bajty pliku przez AES-GCM z 96-bitowym IV i 128-bitowym tagiem uwierzytelniającym. Przepływy klucza publicznego używają RSA-OAEP z kluczami 4096-bitowymi do zawijania klucza symetrycznego. API jest dostępne przez HTTPS w każdej nowoczesnej przeglądarce i działa na natywnym backendzie kryptograficznym, a nie na JavaScript.
Dlaczego SubtleCrypto bije czyste biblioteki JS
window.crypto.subtle wywołuje audytowany natywny backend kryptograficzny przeglądarki, zwykle BoringSSL w Chromium lub CommonCrypto w Safari. W porównaniu z czystymi opcjami JS, takimi jak CryptoJS czy sjcl, SubtleCrypto działa 30–80 razy szybciej dla AES-GCM, unika bocznych kanałów czasowych w interpretatorach JavaScript i nie wysyła użytkownikom żadnych dodatkowych bajtów. Kompromisem jest API oparte na Promise, które operuje tylko na obiektach ArrayBuffer i CryptoKey, więc dużo czasu poświęca się na przepychanie między Uint8Array, Blob a ReadableStream. Dla plików powyżej 100 MB ta infrastruktura ma większe znaczenie niż surowa szybkość kryptograficzna.
Wyprowadzanie klucza z hasła przez PBKDF2
Nigdy nie używaj hasła bezpośrednio jako klucza AES. Zamiast tego zaimportuj hasło jako materiał surowy, a następnie wyprowadź 256-bitowy klucz:
async function deriveKey(password, salt) {
const enc = new TextEncoder();
const material = await crypto.subtle.importKey(
'raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']
);
return crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 210000, hash: 'SHA-256' },
material,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
Wskazówki OWASP na 2026 rok zalecają co najmniej 600 000 iteracji z SHA-256, choć 210 000 pozostaje akceptowalne w kontekstach niskiego ryzyka. Wygeneruj świeży 16-bajtowy salt dla każdego pliku przez crypto.getRandomValues i przechowuj go razem z szyfrogramem. Argon2id byłby mocniejszy, ale jeszcze nie jest dostępny przez SubtleCrypto.
Szyfrowanie pliku przez AES-GCM
AES-GCM zapewnia poufność i autentyczność w jednym przebiegu. Krytyczna zasada: nigdy nie ponownie używaj pary (klucz, IV):
async function encryptFile(file, key) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const plaintext = await file.arrayBuffer();
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
key,
plaintext
);
return { iv, ciphertext };
}
Dla pliku 2 GB file.arrayBuffer() zaalokuje cały bufor, co często powoduje crash mobilnego Safari. Podziel plik na fragmenty po 4 MB, zaszyfruj każdy z unikalnym IV wyprowadzonym z licznika i dodaj bajt wersji oraz salt, żeby deszyfrator wiedział, z czym ma do czynienia.
Strumieniowanie dużych plików przez TransformStream
Aby uniknąć wybuchu pamięci, owiń szyfrowanie w TransformStream i przeprowadź plik przez potok:
const chunkSize = 4 * 1024 * 1024;
const encryptor = new TransformStream({
async transform(chunk, controller) {
const iv = nextIV(counter++);
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, chunk);
controller.enqueue(new Uint8Array([...iv, ...new Uint8Array(ct)]));
}
});
await file.stream()
.pipeThrough(sliceByChunks(chunkSize))
.pipeThrough(encryptor)
.pipeTo(uploadSink);
file.stream() zwraca ReadableStream<Uint8Array> odczytujący z dysku leniwie. Slicer produkuje fragmenty stałego rozmiaru, żeby tagi GCM dopasowywały się przewidywalnie. Szczytowe zużycie pamięci utrzymuje się poniżej 20 MB nawet przy 10 GB uploadu.
Zawijanie klucza symetrycznego przez RSA-OAEP
Kiedy chcesz udostępnić plik konkretnemu odbiorcy, wygeneruj raz jego parę kluczy RSA i opublikuj klucz publiczny:
const keypair = await crypto.subtle.generateKey(
{ name: 'RSA-OAEP', modulusLength: 4096,
publicExponent: new Uint8Array([1,0,1]), hash: 'SHA-256' },
true, ['wrapKey', 'unwrapKey']
);
const wrapped = await crypto.subtle.wrapKey(
'raw', fileKey, keypair.publicKey,
{ name: 'RSA-OAEP' }
);
Klucze RSA 4096-bit zapewniają około 150-bitowego bezpieczeństwa do 2030 roku zgodnie z NIST SP 800-57. Jeśli potrzebujesz forward secrecy lub odporności post-kwantowej, połącz RSA-OAEP z ECDH over P-384 lub zmigruj do ML-KEM (Kyber) gdy grupa robocza WebCrypto go wdroży.
Bezpieczne przechowywanie kluczy w IndexedDB
Obiekty CryptoKey są domyślnie niewyciągalne, co oznacza, że można je utrwalać w IndexedDB bez nigdy eksponowania surowych bajtów do JavaScript:
const db = await openDB('keystore', 1);
await db.put('keys', keypair.privateKey, 'user-signing-key');
Przeglądarki serializują klucz używając algorytmu structured clone i utrzymują rzeczywiste bajty w backendzie kryptograficznym. Skompromitowany skrypt może wywołać encrypt lub decrypt z przechowywanym kluczem, ale nie może odczytać jego materiału. To znaczący krok hartowania w porównaniu do upychania kluczy base64 w localStorage.
Obsługa błędów rzucanych przez API
SubtleCrypto rzuca OperationError przy nieudanym uwierzytelnionym deszyfrowaniu — co zwykle oznacza, że szyfrogram został zmodyfikowany, IV jest zły, albo użytkownik wpisał złe hasło. Rzuca DataError gdy bufor wejściowy ma złą długość, NotSupportedError gdy algorytm nie jest zaimplementowany, a InvalidAccessError gdy klucz nie był zaimportowany z właściwymi flagami użycia. Zawsze owijaj deszyfrowanie w try/catch, wyświetlaj neutralny komunikat "nie można odszyfrować pliku" i unikaj ujawniania, czy tag czy struktura zawiodły.
Praktyczne pułapki
Firefox na Androidzie ogranicza iteracje deriveKey do około miliona przed zamrożeniem wątku UI na kilka sekund — uruchom wyprowadzanie klucza wewnątrz dedykowanego Worker. Safari poniżej 16.4 nie obsługuje crypto.subtle.verify z wypełnieniem PSS. Chrome ogranicza wywołania getRandomValues powyżej 64 KB na wywołanie, więc iteruj jeśli potrzebujesz więcej entropii. Transfery ArrayBuffer przez postMessage są zero-copy, ale niszczą oryginał, co zaskakuje programistów.
HexaTransfer używa dokładnie tego pipeline AES-GCM plus PBKDF2 dla każdego uploadu, z kluczami wyprowadzanymi w Worker i szyfrogramem strumieniowanym do storage bez serwera kiedykolwiek widzącego tekst jawny. Wypróbuj na https://hexatransfer.com — za darmo, bez konta, maks. 10 GB.
Łącząc to wszystko
Minimalny przepływ zaszyfrowanego uploadu: generuj salt i IV przez getRandomValues, wyprowadź klucz AES-GCM z hasła użytkownika przez PBKDF2, przesyłaj strumieniowo plik przez TransformStream szyfrujący każdy fragment 4 MB, dodaj mały nagłówek zawierający wersję, salt i liczbę porcji, i wyślij POST wynik do serwera. Przy pobieraniu odwróć proces fragment po fragmencie, łapiąc OperationError jako sygnał złego hasła lub uszkodzenia. Web Crypto API daje ci wszystko, czego potrzebujesz, a natywna implementacja przeglądarki będzie szybsza od każdej alternatywy JavaScript rzędem wielkości.
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