Mật khẩu, dù băm mạnh và giới hạn tần suất, vẫn có thể bị đánh cắp — qua rò rỉ, qua phishing, qua dùng lại. Xác thực hai bước (2FA) thêm một yếu tố thứ hai: thứ bạn có bên cạnh thứ bạn biết. Bài này đo hai công nghệ 2FA phổ biến nhất — TOTP (mã sáu số trong Google Authenticator) và WebAuthn (khoá bảo mật, vân tay) — để thấy chúng bảo vệ được gì và, quan trọng hơn, cái gì thì không.
Toàn bộ chạy trong container tự dựng, tự dọn. Không nhắm vào hệ thống của ai.
TOTP hoạt động thế nào
TOTP (Time-based One-Time Password) sinh một mã sáu số từ hai thứ: một bí mật chia sẻ giữa server và ứng dụng của bạn lúc thiết lập, và thời gian hiện tại. Cả hai bên tính cùng công thức nên ra cùng mã, mà không cần trao đổi gì qua mạng. Tôi dựng một TOTP thật và đo ba tính chất của nó.
Số đo
Ba lần chạy, kết quả giống hệt:
Mã đổi mỗi 30 giây. Trong suốt một cửa sổ 30 giây, mã không đổi — bốn lần đọc liên tiếp cho cùng một số. Sang cửa sổ mới, mã đổi hoàn toàn.
Cửa sổ chấp nhận là ±31 giây:
| Đồng hồ server lệch | Kết quả |
|---|---|
| −60 giây | từ chối |
| −31 đến +31 giây | chấp nhận |
| +60 giây | từ chối |
TOTP thuần không tự chống phát lại:
| Lần thử | Kết quả |
|---|---|
| Dùng mã lần 1 | hợp lệ, ghi nhận |
| Dùng lại cùng mã (trong 30 giây) | hợp lệ — trừ khi server tự lưu mã đã dùng |
Vì sao TOTP không cần mạng
Một tính chất khiến TOTP hấp dẫn, và cũng là gốc của điểm yếu của nó: nó hoạt động hoàn toàn ngoại tuyến. Lúc thiết lập, server và ứng dụng của bạn thống nhất một bí mật chung (mã QR bạn quét chính là bí mật đó, mã hoá base32). Sau đó, cả hai bên tính mã từ HMAC(bí mật, thời gian) một cách độc lập — điện thoại không cần mạng, server không gửi gì cho bạn. Đó là lý do Google Authenticator chạy được trên máy bay.
Nhưng chính vì mã được tính chỉ từ bí mật và thời gian, nó không chứa thông tin về ngữ cảnh — không biết ai đang hỏi, cho trang nào, trong phiên nào. Nó là một con số đúng-cho-mọi-người-mọi-nơi trong 30 giây. Sự đơn giản đó là sức mạnh (không cần hạ tầng) và là điểm yếu (chuyển tiếp được). WebAuthn đánh đổi ngược lại: nó cần trao đổi qua mạng với server, nhưng nhờ đó gắn được chữ ký vào đúng ngữ cảnh.
Điều đáng nhớ
Cửa sổ ±31 giây là một đánh đổi bắt buộc, không phải lỗi. Đồng hồ của điện thoại và của server không bao giờ khớp tuyệt đối. Nếu server chỉ chấp nhận đúng mã của giây hiện tại, một người dùng có đồng hồ lệch 20 giây sẽ không bao giờ đăng nhập được. Nên server chấp nhận mã của cửa sổ trước và sau (valid_window=1) — mở rộng khoảng hợp lệ thành 90 giây thay vì 30. Đổi lại: nếu kẻ tấn công lấy được mã, nó có tối đa ~90 giây để dùng, không phải 30. Cửa sổ càng rộng càng dễ dùng nhưng càng nhiều thời gian cho kẻ tấn công.
TOTP không tự chống phát lại — và đây là lỗi cấu hình hay bị bỏ sót. Trong 30–90 giây một mã còn hợp lệ, nó dùng được nhiều lần. Nếu kẻ tấn công chặn được một mã (qua phishing, qua malware), nó có thể dùng lại trong cửa sổ đó. Cách chặn: server phải lưu lại mã (hoặc bước thời gian) vừa dùng thành công và từ chối nếu thấy lại. Thư viện TOTP thường không làm việc này cho bạn — verify() chỉ kiểm mã có đúng theo thời gian không, không kiểm đã dùng chưa. Tôi phải tự thêm một tập DA_DUNG để chặn.
Điểm yếu lớn nhất của TOTP: nó không chống được phishing thời gian thực. Mã TOTP chỉ là sáu chữ số — nó không biết bạn đang ở trang nào. Kịch bản tấn công: nạn nhân bị dụ vào một trang phishing giống hệt ngân hàng. Nạn nhân nhập mật khẩu và mã TOTP. Trang phishing chuyển tiếp cả hai tới trang thật ngay lập tức — trong vòng 90 giây mã còn hợp lệ. Trang thật thấy mật khẩu đúng, mã đúng, và cho đăng nhập. TOTP không chặn được điều này, vì nó không có cách nào biết mã đang được nhập vào trang giả.
WebAuthn giải đúng lỗ đó
WebAuthn (khoá bảo mật vật lý, Touch ID, Windows Hello) khác về bản chất: thay vì một mã bạn gõ, nó dùng chữ ký mật mã gắn với tên miền của trang. Khi bạn xác thực, trình duyệt ký một thử thách cùng với origin (tên miền) của trang đang mở. Server kiểm chữ ký đó phải khớp đúng tên miền của nó.
Áp vào kịch bản phishing: nạn nhân ở trang ngan-hang-gia.com, trình duyệt ký thử thách với origin ngan-hang-gia.com. Kẻ tấn công chuyển chữ ký đó tới ngan-hang-that.com, nhưng server thật thấy origin trong chữ ký là ngan-hang-gia.com, không khớp — từ chối. Chữ ký không thể chuyển từ tên miền này sang tên miền khác. Phishing thất bại về mặt toán học, không phải nhờ người dùng cảnh giác.
Đây là khác biệt cốt lõi: TOTP là bí mật con người chuyển tay được; WebAuthn là chữ ký máy gắn chặt với tên miền. Cái đầu chuyển tiếp được nên phishing được; cái sau thì không.
Nghĩa là gì trong thực tế
- Bật 2FA — bất kỳ loại nào — là nâng cấp bảo mật lớn. TOTP dù có điểm yếu phishing vẫn chặn được phần lớn tấn công (rò rỉ mật khẩu, dùng lại, dò trực tuyến). Đừng để "TOTP không hoàn hảo" ngăn bạn bật nó.
- Nếu triển khai TOTP, nhớ tự chống phát lại. Lưu bước thời gian của mã vừa dùng, từ chối nếu thấy lại. Thư viện không làm hộ.
- Với tài khoản giá trị cao, ưu tiên WebAuthn/passkey. Đây là loại 2FA duy nhất chống được phishing thời gian thực — thứ mà TOTP và SMS đều thua.
- SMS là loại yếu nhất (không đo ở đây): ngoài điểm yếu phishing của TOTP, nó còn bị SIM-swap. Dùng khi không có lựa chọn khác, không phải lựa chọn đầu.
Chỗ tôi không kết luận được
Tôi đo TOTP thật nhưng mô phỏng phần WebAuthn bằng lập luận — dựng một luồng WebAuthn đầy đủ cần trình duyệt thật với khoá bảo mật phần cứng hoặc trình xác thực nền tảng, mà container không có. Cơ chế gắn-origin thì rõ ràng và là điểm cốt lõi của chuẩn, nhưng tôi không tự tay đo một cuộc phishing WebAuthn thất bại.
Và tôi không đo mã dự phòng (recovery codes) — thứ mọi hệ 2FA cần để người dùng vào lại khi mất thiết bị. Chúng là điểm yếu thường bị tấn công (lưu ở nơi kém an toàn), và xứng một bài riêng.
Thử ba mươi giây
Vào phần cài đặt bảo mật của một tài khoản quan trọng. Nó cho bạn những lựa chọn 2FA nào?
Nếu chỉ có SMS, đó là mức yếu nhất — tìm xem có TOTP không. Nếu có TOTP, bật nó ngay. Và nếu có "khoá bảo mật" hay "passkey", đó là lựa chọn mạnh nhất chống phishing — bật nó cho tài khoản bạn không thể để mất, như email chính (nơi mọi liên kết đặt lại mật khẩu khác đổ về).