Bezpieczna generacja liczb losowych: fundament silnej kryptografii
Bezpieczna generacja losowa jest krytyczna dla szyfrowania. Jak działa crypto.getRandomValues i dlaczego słaba losowość łamie bezpieczeństwo.
Bezpieczna generacja liczb losowych to fundament, na którym opiera się każdy inny element kryptografii. W JavaScript crypto.getRandomValues(buffer) wypełnia TypedArray kryptograficznie bezpiecznymi losowymi bajtami z systemowego CSPRNG (/dev/urandom na Linux/macOS, BCryptGenRandom na Windows, SecRandomCopyBytes na iOS/macOS). Nie używaj Math.random() do niczego związanego z bezpieczeństwem — to PRNG w stylu Mulberry32 lub xorshift, zaprojektowany dla szybkości, a nie nieprzewidywalności, a jego wyniki są przewidywalne po zaobserwowaniu kilku próbek. Słaby RNG psuje klucze AES, uzgodnienia TLS, unikalność nonce w GCM, nieprzewidywalność tokenów i każdy inny prymityw bezpieczeństwa zależny od nieprzewidywalnych bitów.
Różnica między losowym a kryptograficznie losowym
PRNG (pseudo-random number generator) produkuje deterministyczny strumień z nasienia. Znając nasienie i algorytm, możesz odtworzyć każdy wynik. Odpowiedni do gier, symulacji i metod Monte Carlo. Katastrofalny dla kryptografii.
CSPRNG (cryptographically secure PRNG) jest zasilany z prawdziwego źródła entropii (szum termiczny, timing przerwań, sprzętowe instrukcje RNG jak Intel RDSEED) i jego projekt zapewnia, że wyniki są obliczeniowo nieodróżnialne od prawdziwej losowości, a obserwowanie wcześniejszych wyników nie pomaga w przewidywaniu przyszłych.
JavaScript daje Ci obydwa. Math.random() to PRNG. crypto.getRandomValues() to opakowanie na systemowy CSPRNG. Jedna linia kodu różnicy, ogromna różnica w bezpieczeństwie.
Kanoniczne poprawne użycie
// Generuj 256 bitów losowych bajtów dla klucza AES
const keyBytes = crypto.getRandomValues(new Uint8Array(32));
// Generuj 96-bitowy nonce GCM
const nonce = crypto.getRandomValues(new Uint8Array(12));
// Generuj 128-bitową sól
const salt = crypto.getRandomValues(new Uint8Array(16));
// Generuj bezpieczny token URL
const tokenBytes = crypto.getRandomValues(new Uint8Array(32));
const token = btoa(String.fromCharCode(...tokenBytes))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
crypto.getRandomValues() jest synchroniczny, wypełnia bufor w miejscu, zwraca bufor. Maksymalny rozmiar żądania to 65 536 bajtów w jednym wywołaniu (limit narzucony przez specyfikację, by zapobiec blokowaniu). Dla większego materiału losowego wywołuj wielokrotnie.
Odpowiedniki w Node.js
const { randomBytes, randomFillSync, webcrypto } = require('crypto');
const keyBytes = randomBytes(32); // Zwraca Buffer
// Lub kompatybilne z Web Crypto
const nonce = webcrypto.getRandomValues(new Uint8Array(12));
randomBytes Node pobiera z tego samego CSPRNG co Web Crypto. Używaj dowolnego stylu API pasującego do Twojego kodu. Dla kodu izomorficznego działającego w obu środowiskach, webcrypto.getRandomValues dokładnie odpowiada przeglądarce.
Dlaczego Math.random zawodzi
V8 (Chrome/Node), SpiderMonkey (Firefox) i JavaScriptCore (Safari) implementują Math.random() jako szybki PRNG bez gwarancji kryptograficznych. V8 używa wariantu xorshift128+. Badacze wykazali, że po zaobserwowaniu ~5 wyników atakujący może odzyskać stan wewnętrzny i przewidywać wszystkie przyszłe wyniki. W 2015 roku Mike Pound i współpracownicy odwrócili stan Math.random V8 w rzeczywistych programach bug bounty.
Jeśli używasz Math.random() do generowania tokenów sesji, linków do resetowania hasła, nonce szyfrowania lub ID udostępniania, atakujący obserwujący kilka z nich może przewidzieć resztę. To nie jest teoretyczne — to powszechna klasa błędów w audytach.
Typowe błędy użycia
Zasilanie biblioteki PRNG przez Math.random():
// ŹLE
const seed = Math.floor(Math.random() * 2**32);
Nic poniżej nie może być bardziej losowe niż nasienie. Zamiast tego użyj crypto.getRandomValues(new Uint32Array(1))[0].
Używanie Date.now() jako entropii: Czas jest odgadywalny w wąskich oknach. Nawet połączony z niewielkim czynnikiem losowym, znaczniki czasu ujawniają wystarczająco dużo bitów dla atakujących.
Własne XOR źródeł: Nie rób tego. Systemowe CSPRNG już mieszają każde przydatne źródło entropii. Dodawanie własnego mieszania zazwyczaj zmniejsza entropię zamiast ją zwiększać.
Bias modulo przy generowaniu zakresów: randomBytes[0] % 10 nie jest równomiernie rozłożony na 0–9, bo 256 nie jest wielokrotnością 10. Dla równomiernych losowych liczb całkowitych w zakresie używaj odrzucania próbek:
function randomInt(max) {
const range = new Uint32Array(1);
const threshold = 2**32 - (2**32 % max);
do {
crypto.getRandomValues(range);
} while (range[0] >= threshold);
return range[0] % max;
}
Źródła entropii i problemy przy uruchamianiu systemu
Na Linux, /dev/urandom jest zawsze bezpieczny po wczesnym etapie bootowania. Przez kilka pierwszych sekund uruchamiania systemów bez sprzętowego RNG, pula jądra może być niewystarczająco zasilona. Zostało to wykorzystane w błędzie Debian OpenSSL z 2008 roku, gdy poprawka usunęła mieszanie entropii i pozostawiła tylko ID procesu jako nasienie. Klucze wygenerowane w tym oknie miały tylko 2^15 możliwych wartości — wyliczone w sekundy.
Nowoczesne systemy zasilają CSPRNG jądra z: RDSEED na x86-64 (gdy dostępny), instrukcji RNG ARMv8.5-A, szumu termicznego z różnych urządzeń peryferyjnych, timingu przerwań, klawiatury/myszy gdy interaktywny. Na serwerach z Intel Ice Lake lub AMD Zen 3+, CSPRNG jest zasilany w ciągu mikrosekund od startu.
Dla kontenerów Docker: /dev/urandom hosta jest przekazywany domyślnie. Żadnych działań nie potrzeba. Dla bezserwerowych (AWS Lambda, Cloudflare Workers) runtime obsługuje zasilanie entropii per wywołanie.
Tokeny sesji i ID udostępniania
Dla usługi transferu pliku generujesz losowe identyfikatory dla:
- ID pliku w URL (atakujący nie powinni móc zgadnąć prawidłowych ID)
- Tokenów udostępniania dla linków chronionych hasłem
- Tokenów CSRF
- Kluczy szyfrowania (klucze AES per plik)
- Nonce dla GCM
Minimalna długość: 128 bitów (16 bajtów) dla kolizji i nieprzewidywalności, 256 bitów (32 bajty) dla kluczy. Bezpieczne kodowanie przez base64url dodaje ~33% długości; hex dodaje 100%.
32-bajtowy token zakodowany base64url ma 43 znaki i jest praktycznie wolny od kolizji przy 2^256.
Testowanie pod kątem słabego RNG
Oznaki, że Twój RNG jest zepsuty lub słaby:
- Identyczne tokeny generowane przez różne żądania (kolizja w tym, co powinno być ogromną przestrzenią)
- Wyniki przechodzą testy wizualne, ale nie przechodzą baterii statystycznych
dieharderlubPractRand - Ponowne użycie nasienia po restarcie procesu — każde wdrożenie używa tego samego stanu początkowego
- Generowane klucze mają wzorce (np. pierwsze 4 bajty różnią się, ale ostatnie 28 są identyczne)
W produkcji prawdopodobnie tego nie zobaczysz, chyba że coś jest katastrofalnie zepsute. Tryb awarii jest zazwyczaj cichy: ataki po prostu stają się wykonalne na tym, co powinno być przestrzenią 2^256.
Audyt: każde wywołanie Math.random() w kodzie powinno być przejrzane. Grep po Math.random w drzewie źródłowym to dobra tygodniowa praktyka higieny. Konwersja każdego miejsca istotnego dla bezpieczeństwa na crypto.getRandomValues zajmuje minuty i zapobiega realnym podatnościom.
Losowe ciągi i UUID
Dla identyfikatorów widocznych dla człowieka, crypto.randomUUID() zwraca UUID v4 (122 bity losowości) w standardowym formacie:
const id = crypto.randomUUID();
// "f47ac10b-58cc-4372-a567-0e02b2c3d479"
Obsługiwane w Chrome 92+, Firefox 95+, Safari 15.4+, Node 14.17+. Odpowiednie dla kluczy głównych baz danych, ID żądań API i niekrytycznych dla bezpieczeństwa unikalnych identyfikatorów. Używaj jawnego getRandomValues dla wszystkiego wymagającego niestandardowych formatów lub wyższej entropii.
Na serwerach: unikaj własnych pul RNG
Niektóre frameworki serwerowe oferują własne pule losowe, które twierdzą, że „mieszają" systemowy CSPRNG z entropią na poziomie aplikacji. Traktuj to z podejrzliwością. Własne mieszanie rzadko poprawia wynik jądra i może cicho zmniejszyć entropię w razie błędu.
Jeśli jesteś na Node lub głównym runtimie, wbudowany crypto.randomBytes jest poprawny i szybki. Nie zastępuj go mieszarkami firm trzecich.
Wniosek
Każdy element kryptografii w aplikacji transferu pliku zależy od nieprzewidywalnych losowych bajtów. Klucze AES, nonce GCM, sole PBKDF2, tokeny udostępniania, tokeny anty-CSRF, ID sesji — wszystkie potrzebują tego samego prymitywu: crypto.getRandomValues() w przeglądarkach, crypto.randomBytes() lub webcrypto.getRandomValues w Node. Używaj ich. Nigdy Math.random(). Nigdy znaczników czasu. Nigdy własnego mieszania.
HexaTransfer wyprowadza każdy klucz AES per plik, nonce i identyfikator URL z crypto.getRandomValues() po stronie klienta. ID udostępniania po stronie serwera pochodzą z crypto.randomBytes. Jedno API, spójne zachowanie, brak możliwości przypadkowego wprowadzenia przewidywalnych bitów do systemu.
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