Bài AES trước kết bằng lời khuyên: CBC không tự xác thực nên dễ dính padding oracle — hãy dùng GCM. Bài này giải thích vì sao. AES-GCM là authenticated encryption (AEAD): nó không chỉ giấu nội dung mà còn chứng minh bản mã chưa bị sửa. Đây là chế độ mặc định nên dùng cho mã hóa đối xứng ngày nay (TLS 1.3 chỉ còn AEAD). Nhưng GCM có một cái bẫy chết người: nonce không bao giờ được lặp. Bài này đo thật cả sức mạnh lẫn cái bẫy đó bằng python cryptography.
Vì sao cần mã hóa CÓ xác thực (AEAD)
Với CBC (bài trước), bạn giấu được nội dung, nhưng nếu kẻ tấn công lật vài bit trong bản mã, khi giải mã bạn nhận về rác mà không biết nó đã bị sửa — và trong nhiều giao thức, chính phản ứng với rác đó làm lộ thông tin (padding oracle). AEAD gộp hai việc vào một:
encrypt(key, nonce, plaintext) -> ciphertext + tag (16 byte)
Khi giải mã, GCM kiểm tag trước: nếu tag không khớp (bản mã bị sửa, sai khóa, sai nonce), nó từ chối — không trả về plaintext rác. Nhờ vậy GCM chống cả nghe lén lẫn giả mạo trong một thao tác.
Ba thành phần: nonce (số dùng một lần ~12 byte, gửi kèm, không bí mật), tag (~16 byte, mã xác thực gắn liền bản mã), và AAD tùy chọn (dữ liệu được xác thực nhưng không mã hóa — ví dụ header gói tin cần đọc được mà vẫn chống sửa).
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
aes = AESGCM(key)
ct = aes.encrypt(nonce, plaintext, aad) # ct = ciphertext + tag
pt = aes.decrypt(nonce, ct, aad) # sai tag -> ném InvalidTag

Hình 1: GCM = mã hóa + tag xác thực trong một bước; giải mã kiểm tag trước nên chống cả nghe lén lẫn giả mạo. Ba thành phần nonce/tag/AAD. Quy tắc vàng: nonce không bao giờ lặp với cùng khóa.
Đo thật: mã hóa, phát hiện sửa đổi

Hình 2: Mã hóa ra bản mã a2123c85... kèm tag 06bbf7cb.... Giải mã đúng → plaintext gốc. Sửa một byte hay sai nonce → InvalidTag (từ chối, không trả rác — khác hẳn CBC). Nonce lặp: c1⊕c2 = p1⊕p2 giống hệt → lộ XOR hai bản rõ mà không cần khóa.
Kết quả:
- Mã hóa: bản rõ ra
a2123c85...kèm tag06bbf7cb...(16 byte gắn liền). Bản mã dài hơn bản rõ đúng 16 byte tag. - Giải mã đúng: nonce + tag khớp → trả về "chuyen 100 trieu cho B".
- Phát hiện giả mạo: sửa một byte bản mã →
InvalidTag, GCM từ chối giải mã, không trả rác. Sai nonce cũngInvalidTag. Đây là khác biệt cốt lõi với CBC: CBC vẫn "giải mã" ra dữ liệu rác mà không báo lỗi, để ứng dụng tự xử lý (và tự dính bẫy). - Cái bẫy nonce lặp: mã hóa hai bản rõ khác nhau với cùng khóa và cùng nonce, rồi XOR hai bản mã (
c1⊕c2) ra kết quả giống hệt XOR hai bản rõ (p1⊕p2). Nghĩa là kẻ tấn công biếtp1⊕p2mà không cần khóa — từ đó suy được nội dung nếu biết một trong hai, hay khai thác cấu trúc. Tệ hơn, lặp nonce trong GCM còn phá khả năng xác thực, cho phép giả mạo tag. Vì thế nonce phải duy nhất với mỗi khóa — tuyệt đối.
Đánh đổi và lưu ý
Nonce: ngẫu nhiên hay đếm — nhưng không bao giờ lặp. Với nonce 96 bit (12 byte, chuẩn GCM), sinh ngẫu nhiên mỗi lần thường an toàn cho tới ~2³² thông điệp/khóa (rủi ro trùng ngẫu nhiên). Nếu mã hóa cực nhiều, dùng bộ đếm tăng dần (đảm bảo không trùng) hoặc đổi khóa định kỳ. Điều tối kỵ: dùng nonce cố định (0000...) hay tái dùng — lỗi đã hạ nhiều hệ thống thật.
Tag phải được kiểm, đừng cắt ngắn tùy tiện. Tag 128 bit là mặc định; cắt ngắn tag làm giảm sức chống giả mạo. Và luôn để thư viện tự kiểm tag khi giải mã — đừng tự so tag bằng tay (dễ sai, dễ timing attack). AESGCM.decrypt ném InvalidTag là đúng: bắt lỗi đó và coi dữ liệu là không hợp lệ, đừng dùng bất cứ phần nào.
GCM vs ChaCha20-Poly1305. GCM nhanh khi CPU có lệnh AES-NI (hầu hết server/PC hiện đại). Trên thiết bị không có tăng tốc AES (một số mobile/IoT), ChaCha20-Poly1305 (cũng là AEAD) thường nhanh hơn và ít rủi ro cài đặt sai. TLS 1.3 hỗ trợ cả hai. Nếu nền tảng đa dạng, cân nhắc ChaCha20-Poly1305.
Ba ý mang về
- AES-GCM là AEAD — mã hóa + xác thực trong một bước: đo thật, ra bản mã kèm tag 16 byte; sửa một byte hay sai nonce →
InvalidTag, từ chối giải mã (không trả rác như CBC). Đây là chế độ mặc định nên dùng. - Nonce KHÔNG BAO GIỜ được lặp với cùng khóa: đo thật, lặp nonce làm
c1⊕c2=p1⊕p2— lộ XOR hai bản rõ mà không cần khóa, và phá luôn xác thực. Dùng nonce ngẫu nhiên mật mã hoặc bộ đếm không trùng. - Dùng đúng và dùng thư viện: để thư viện tự kiểm tag (bắt
InvalidTag= coi như dữ liệu hỏng, bỏ hết); tag 128 bit; và cân nhắc ChaCha20-Poly1305 trên nền không có AES-NI.
Nguồn
- NIST SP 800-38D (GCM) và cảnh báo tái dùng nonce: https://csrc.nist.gov/pubs/sp/800/38/d/final
- Python cryptography — AESGCM: https://cryptography.io/en/latest/hazmat/primitives/aead/
Phần sau ta chuyển sang mã hóa bất đối xứng: RSA — khóa công khai và khóa riêng, khác biệt giữa mã hóa và ký, và vì sao RSA thường chỉ dùng để bọc khóa chứ không mã hóa dữ liệu lớn.