Przejdź do treści
HexaTransfer
Wróć do bloga
Szyfrowanie i bezpieczenstwo

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:

  1. 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.
  2. 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ć OperationError w 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").webcrypto z 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