Suốt sê-ri này, mọi thứ đều dựa vào một thứ tưởng chừng tầm thường: số ngẫu nhiên không đoán được. Khóa AES, salt mật khẩu, IV/nonce, token phiên, mã OTP, khóa riêng — tất cả chỉ an toàn khi được sinh từ nguồn ngẫu nhiên thật sự khó đoán. Đây là nơi vô số lỗ hổng nghiêm trọng bắt nguồn: lập trình viên dùng random() thường (vốn tất định) để sinh giá trị bảo mật. Bài này đo thật khác biệt giữa PRNG thường và CSPRNG, và vì sao chọn sai là sập cả hệ mật mã. Chạy thật bằng python3 và openssl.

Hai loại "ngẫu nhiên" khác nhau hoàn toàn

  • PRNG thường (random, Math.random, rand): tất định — sinh dãy số từ một seed. Cùng seed cho cùng dãy. Nhanh, phân bố đẹp, hoàn hảo cho game, mô phỏng, xáo trộn dữ liệu. Nhưng vì tất định và (với nhiều thuật toán như Mersenne Twister) đảo ngược được nếu quan sát đủ output → tuyệt đối không dùng cho bảo mật.
  • CSPRNG (Cryptographically Secure PRNG: secrets, os.urandom, openssl rand, SecureRandom): lấy entropy từ hệ điều hành (/dev/urandom), không seed thủ công, output không đoán được kể cả khi biết mọi output trước đó → dùng cho mọi giá trị bảo mật.
# CSPRNG:
python3 -c 'import secrets; print(secrets.token_hex(16))'
openssl rand -hex 16
head -c 16 /dev/urandom | xxd -p

Ảnh chụp đoạn mã nền tối giải thích random an toàn vì sao random thường không dùng cho bảo mật, hai loại ngẫu nhiên hoàn toàn khác nhau PRNG thường random Math.random rand tất định từ một seed cùng seed ra cùng dãy nhanh hợp game mô phỏng xáo trộn đoán được nếu biết đoán được seed cấm dùng cho bảo mật, CSPRNG secrets os.urandom openssl rand lấy entropy từ HĐH không seed thủ công không đoán tái tạo được cho bảo mật, vì sao PRNG nguy hiểm cho token PRNG sinh từ seed nhiều người seed bằng thời gian timestamp kẻ tấn công đoán khoảng thời gian thử vài nghìn seed tái tạo đúng token OTP mã reset mật khẩu bạn đã sinh đã có vụ thật token dự đoán được vì dùng random seed yếu, quy tắc mọi giá trị bí mật ngẫu nhiên an toàn dùng CSPRNG token phiên mã OTP reset salt IV nonce khóa AES khóa API python secrets token_hex os.urandom shell openssl rand head -c dev urandom Java SecureRandom JS node crypto randomBytes tránh random.random Math.random rand cho bảo mật

Hình 1: PRNG thường tất định từ seed (đoán được → cấm dùng bảo mật); CSPRNG lấy entropy HĐH, không đoán được. Mọi giá trị bí mật (token, salt, IV, nonce, khóa) phải từ CSPRNG.

Đo thật: tất định vs không đoán được

Ảnh chụp bảng kết quả đo thật nền tối chạy python3 và openssl, phần một random thường cùng seed ra cùng dãy random.seed 42 lần 1 ra 2824 1409 5506 5012 random.seed 42 lần 2 ra 2824 1409 5506 5012 giống hệt, phần hai token sinh bằng random thường tái tạo được seed timestamp lần 1 ra 5e7982e4d51b9992 seed timestamp lần 2 ra 5e7982e4d51b9992 tái tạo được nguy hiểm, phần ba CSPRNG secrets os.urandom khác mỗi lần secrets.token_hex 16 ra d81e68f17bc2d3d7508b868bc3f60b03 secrets.token_hex 16 ra 356fd156fe2ca2a951ec450f0173af34 os.urandom 8 hex ra 9308db844a81e463 không seed không đoán, phần bốn openssl rand nguồn entropy của HĐH openssl rand -hex 16 ra a789824fd673b8996af1c5b28b657c4d openssl rand -hex 16 ra 0234fcad20799e1df34b378e19dec181

Hình 2: random.seed(42) cho [2824, 1409, 5506, 5012] hai lần giống hệt. Token sinh bằng random với seed=timestamp tái tạo được (5e7982e4d51b9992 cả hai lần). CSPRNG (secrets, os.urandom, openssl rand) cho giá trị khác mỗi lần, không đoán được.

Kết quả phơi bày rủi ro:

  • PRNG tất định: random.seed(42) rồi lấy 4 số cho [2824, 1409, 5506, 5012] — chạy lại với cùng seed ra y hệt. Đây là tính năng (tái lập được cho test/mô phỏng), nhưng là lỗ hổng nếu dùng sinh giá trị bí mật.
  • Token tái tạo được: sinh một token 16 ký tự bằng random với seed = timestamp — hai lần cùng seed ra 5e7982e4d51b9992 giống hệt. Nếu app seed bằng thời gian (rất phổ biến), kẻ tấn công đoán khoảng thời gian bạn sinh token, thử vài nghìn seed, tái tạo đúng token/OTP/mã reset mật khẩu. Đã có nhiều sự cố thật kiểu này.
  • CSPRNG không đoán được: secrets.token_hex(16) cho d81e68f1... rồi 356fd156... — khác nhau mỗi lần, không seed, không tái tạo. os.urandom và openssl rand tương tự. Đây là nguồn đúng cho mọi giá trị bảo mật.

Đánh đổi và lưu ý

Dùng đúng hàm cho đúng việc. PRNG thường không xấu — nó nhanh và tái lập được, hoàn hảo cho mô phỏng, game, chia batch, xáo trộn dữ liệu huấn luyện. CSPRNG chậm hơn chút và không tái lập, dành cho bảo mật. Đừng dùng CSPRNG cho mô phỏng cần tái lập, và tuyệt đối đừng dùng PRNG cho token/khóa. Quy tắc: hỏi "nếu kẻ tấn công đoán được giá trị này thì có hại không?" — có thì CSPRNG.

Biết hàm nào là CSPRNG trong ngôn ngữ của bạn. Python: secrets / os.urandom (không phải random). Java: SecureRandom (không phải Random / Math.random). Node: crypto.randomBytes / crypto.randomUUID (không phải Math.random). Go: crypto/rand (không phải math/rand). Nhầm hai họ này là lỗi kinh điển — tên rất giống nhau nên dễ import nhầm.

Đủ độ dài và đủ entropy. Token/khóa phải đủ dài để không brute-force được: token phiên ≥ 128 bit (16 byte), khóa AES 256 bit (32 byte). /dev/urandom trên hệ hiện đại luôn đủ entropy sau khi khởi động; đừng dùng /dev/random (có thể block) trừ nhu cầu đặc biệt. Trong container/VM mới tinh, chú ý entropy lúc boot sớm.

Ba ý mang về

  1. PRNG thường tất định — cấm dùng cho bảo mật: đo thật, random.seed(42) ra dãy y hệt hai lần, token seed=timestamp tái tạo được (5e7982e4d51b9992) — kẻ tấn công đoán seed là đoán ra token/OTP/mã reset.
  2. CSPRNG không đoán được, dùng cho mọi giá trị bí mật: secrets/os.urandom/openssl rand cho giá trị khác mỗi lần, không seed — dùng cho token, salt, IV/nonce, khóa, mã OTP.
  3. Chọn đúng hàm và đủ độ dài: biết hàm CSPRNG trong ngôn ngữ (Python secrets, Java SecureRandom, Node crypto, Go crypto/rand — không nhầm với họ random/math); token ≥16 byte, khóa AES 32 byte; hỏi "kẻ tấn công đoán được có hại không?" để chọn.

Nguồn

Phần sau ta ghép mọi mảnh lại thành việc thực tế: mã hóa một file bằng mật khẩu — AES với khóa dẫn từ mật khẩu qua PBKDF2, đo thật bằng openssl enc.