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))

Ảnh chụp đoạn mã nền tối giải thích TOTP mã 6 số của 2FA ghép HMAC cộng thời gian, mã OTP trên app không hề gửi qua mạng hiểu lầm mã 6 số được server gửi tới app KHÔNG TOTP server và app chia sẻ một bí mật lúc quét mã QR rồi cả hai tự tính ra cùng mã dựa trên bí mật cộng thời gian không cần mạng lúc sinh mã app offline vẫn ra đúng mã, thuật toán TOTP RFC 6238 bốn bước 1 counter bằng thời gian hiện tại chia 30 cửa sổ 30 giây 2 h bằng HMAC-SHA1 bí mật counter 8 byte 3 dynamic truncation lấy 4 byte theo offset từ byte cuối 4 mã bằng 4 byte đó chia lấy dư 1000000 ra 6 chữ số, tự tính bằng python không thư viện TOTP import hmac hashlib struct base64 time key base64.b32decode JBSWY3DPEHPK3PXP counter int time.time chia 30 h hmac.new key struct.pack counter sha1 digest off h cuối 0x0f code struct.unpack 4 byte chia dư 1000000 zfill 6 đây đúng là thứ Google Authenticator làm

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:

Ảnh chụp bảng kết quả đo thật nền tối chạy python3 RFC 6238 bí mật base32 JBSWY3DPEHPK3PXP cửa sổ 30 giây, phần một tính mã tại một mốc thời gian t bằng 1700000000 counter bằng 56666666 t chia 30 mã TOTP 324550, phần hai hai bên cùng bí mật cùng mốc cùng mã client 324550 server 324550 KHỚP đồng bộ không cần mạng, phần ba trong cùng cửa sổ 30s mã không đổi t bằng 1700000010 counter 56666667 mã 367665 t bằng 1700000029 counter 56666667 mã 367665 cùng counter cùng mã, phần bốn sang cửa sổ mới mã đổi t bằng 1700000040 counter 56666668 mã 870960 counter tăng mã mới

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=1700000010 và t=1700000029 cùng thuộc counter 56666667 → 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 → counter 56666668 → 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ề

  1. 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).
  2. 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).
  3. 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

Đế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.