Bài cuối sê-ri ghép mọi thứ đã học — hash, HMAC, bí mật chung, thời gian — thành một thứ bạn dùng gần như mỗi ngày: mã 6 số của xác thực hai lớp (2FA) trên Google Authenticator, Authy, hay app ngân hàng. Hiểu lầm phổ biến nhất: nghĩ rằng mã đó được server gửi tới điện thoại. Không hề — điện thoại tự tính ra mã, offline cũng được. Cơ chế là TOTP (Time-based One-Time Password, RFC 6238), và nó đơn giản đến mức ta tự viết lại trong 6 dòng python. Đo thật bằng python3.
Mã OTP không hề đi qua mạng
TOTP dựa trên một bí mật chung được thiết lập một lần (lúc bạn quét mã QR để thêm tài khoản — mã QR chính là bí mật đó ở dạng base32). Từ đó về sau:
- Server và app cùng giữ bí mật đó.
- Cả hai tự tính ra mã 6 số dựa trên bí mật + thời gian hiện tại.
- Vì đồng hồ hai bên gần khớp, mã hai bên tính ra giống nhau → server so mã bạn nhập với mã nó tự tính. Không có mã nào truyền qua mạng lúc sinh.
Thuật toán TOTP (RFC 6238) gồm 4 bước, xây trên HMAC (bài mat-ma-03):
1. counter = thời_gian_hiện_tại // 30 # chia thành cửa sổ 30 giây
2. h = HMAC-SHA1(bí_mật, counter dạng 8 byte)
3. dynamic truncation: lấy 4 byte tại offset đọc từ byte cuối của h
4. mã = (4 byte đó) % 1000000 # còn 6 chữ số
import hmac, hashlib, struct, base64, time
key = base64.b32decode("JBSWY3DPEHPK3PXP") # bí mật base32
counter = int(time.time()) // 30
h = hmac.new(key, struct.pack(">Q", counter), hashlib.sha1).digest()
off = h[-1] & 0x0f
code = (struct.unpack(">I", h[off:off+4])[0] & 0x7fffffff) % 1000000
print(str(code).zfill(6))

Hình 1: TOTP không gửi mã qua mạng — server và app cùng tính từ bí mật chung + thời gian. Thuật toán 4 bước (RFC 6238): counter = time//30 → HMAC-SHA1 → dynamic truncation → 6 chữ số. Tự viết được trong 6 dòng python.
Đo thật: tự tính mã TOTP
Dùng một bí mật base32 cố định và các mốc thời gian cố định để tái lập:

Hình 2: Tại t=1700000000 (counter 56666666) mã là 324550; hai bên cùng bí mật ra cùng mã (KHỚP). Trong cùng cửa sổ (t=1700000010 và 1700000029, counter 56666667) mã 367665 không đổi; sang cửa sổ mới (t=1700000040, counter 56666668) mã đổi thành 870960.
Kết quả đúng như RFC 6238:
- Tính mã:
t=1700000000→counter = 56666666(t chia 30) → mã324550. Không có gì ngẫu nhiên — hoàn toàn xác định từ bí mật + counter. - Hai bên đồng bộ: client và server cùng bí mật, cùng mốc thời gian → cùng ra
324550, KHỚP. Đây là cách server xác thực mà không cần gửi/nhận mã qua mạng lúc sinh. - Không đổi trong cửa sổ 30s:
t=1700000010vàt=1700000029cùng thuộc counter56666667→ cùng mã367665. Nhờ chia thời gian thành cửa sổ 30 giây, mã ổn định đủ lâu để bạn kịp gõ. - Đổi sang cửa sổ mới:
t=1700000040→ counter56666668→ mã870960. Cứ mỗi 30 giây counter tăng một, mã đổi hoàn toàn (nhờ avalanche của HMAC/hash).
Đánh đổi và lưu ý
Cửa sổ dung sai cho lệch đồng hồ. Đồng hồ điện thoại và server không khớp tuyệt đối. Nên server thường chấp nhận mã của cửa sổ hiện tại ± 1 (tức ±30 giây) để không từ chối oan. Đổi lại, cửa sổ càng rộng thì kẻ tấn công càng nhiều thời gian thử — cân bằng ±1 là chuẩn thực tế.
Chống dùng lại mã (replay). Mã TOTP hợp lệ trong ~30–60 giây; trong cửa sổ đó nếu kẻ tấn công chộp được (phishing) có thể dùng lại. Phòng thủ: server ghi nhớ mã đã dùng trong cửa sổ và từ chối lần hai; và với thao tác nhạy cảm, TOTP không thay được khóa bảo mật phần cứng (FIDO2/WebAuthn — chống phishing tốt hơn vì gắn với tên miền).
Bí mật TOTP phải được bảo vệ như mật khẩu. Nếu bí mật base32 lộ, kẻ tấn công tự sinh mã như bạn — 2FA vô hiệu. Server phải lưu bí mật này mã hóa, và mã QR chỉ hiện một lần lúc thiết lập. Dùng HMAC-SHA1 ở đây là theo chuẩn (tương thích mọi app), an toàn cho mục đích này dù SHA-1 đã yếu cho va chạm — vì TOTP dựa vào tính một chiều của HMAC, không phải chống va chạm.
Ba ý mang về
- TOTP không gửi mã qua mạng: server và app cùng bí mật, mỗi bên tự tính mã từ bí mật + thời gian — đo thật, cùng bí mật + cùng mốc ra cùng mã
324550(đồng bộ, offline vẫn đúng). - Thuật toán là HMAC + thời gian, tự viết được:
counter = time//30→HMAC-SHA1→ dynamic truncation → 6 chữ số; mã không đổi trong cửa sổ 30s (367665) rồi đổi sang cửa sổ mới (870960). - Dùng đúng: server chấp nhận cửa sổ ±1 (lệch đồng hồ), chống dùng lại mã, lưu bí mật mã hóa; TOTP tốt nhưng vẫn bị phishing — thao tác nhạy cảm cân nhắc FIDO2/WebAuthn.
Nguồn
- RFC 6238 (TOTP) và RFC 4226 (HOTP): https://www.rfc-editor.org/rfc/rfc6238
- Python docs — hmac / base64: https://docs.python.org/3/library/hmac.html
Đến đây sê-ri Mật mã cho lập trình viên khép lại 12 phần: hàm băm, băm mật khẩu, HMAC, AES và các chế độ, AES-GCM, RSA, đường cong elliptic, chữ ký số, Diffie-Hellman, ngẫu nhiên an toàn, mã hóa file, và TOTP. Sợi chỉ xuyên suốt: mật mã không phải phép màu hay toán bất khả xâm phạm — nó là vài viên gạch (hàm một chiều, khóa đối xứng/bất đối xứng, ngẫu nhiên khó đoán) ghép theo đúng cách. Đa số lỗ hổng đến từ dùng sai các viên gạch đó (SHA cho mật khẩu, ECB, nonce lặp, random yếu), chứ không phải thuật toán bị phá. Nắm được dùng cái gì cho việc gì — và luôn dùng thư viện chuẩn thay vì tự chế — là đủ để viết phần mềm an toàn.