Xác thực đa yếu tố (MFA) tạo nên phòng tuyến cơ bản chống lại các cuộc tấn công credential stuffing, lừa đảo (phishing) và chiếm quyền điều khiển phiên (session hijacking) trên khắp các hệ thống phân tán hiện đại. Trong số các cơ chế xác thực thứ cấp, Mật khẩu Một lần Dựa trên Thời gian (TOTP) đại diện cho một trong những tiêu chuẩn mở được triển khai rộng rãi nhất. Được quy định bởi Internet Engineering Task Force (IETF) trong RFC 6238, TOTP điều phối các chuỗi mã số gồm sáu chữ số hoặc tám chữ số xoay vòng được tạo ra bởi các ứng dụng xác thực trên toàn thế giới.
Khác với các thông báo đẩy (push notifications) độc quyền hay mã xác minh qua Dịch vụ Tin nhắn Ngắn (SMS), TOTP hoạt động hoàn toàn tách biệt khỏi quá trình truyền tải mạng tức thời. Tác nhân người dùng (user agent) và máy chủ xác thực tính toán các token xác thực giống hệt nhau một cách độc lập, chỉ được đồng bộ hóa duy nhất bằng thời gian Unix đã trôi qua và một khóa mã hóa được chia sẻ trước. Việc thấu hiểu nền tảng mật mã học của TOTP đòi hỏi phải mổ xẻ các nguyên thủy cốt lõi của nó: sự chuyển đổi thời gian liên tục thành các bước rời rạc, cấu trúc bên trong của Mã xác thực Thông điệp Dựa trên Hàm băm (HMAC), thuật toán dynamic truncation được định nghĩa trong RFC 4226, cùng các đánh đổi bảo mật thực tế chi phối độ lệch đồng hồ, cửa sổ tra cứu và việc phân tích cú pháp khóa bí mật cục bộ trên trình duyệt.
Đối với các kỹ sư muốn xác thực trực tiếp quy trình tạo token, việc sử dụng một công cụ tạo mã TOTP trên trình duyệt cho phép kiểm tra trực tiếp hành vi của khóa bí mật bằng các công cụ web bảo toàn quyền riêng tư. Khi khởi tạo tài khoản, các hệ thống thường tuần tự hóa những khóa này thành mã vạch ma trận, khiến việc trích xuất khóa bí mật TOTP bằng bộ giải mã QR trở nên thiết yếu để kiểm tra định dạng seed và các tham số truy vấn URI.
1. Nền tảng của Xác thực Một lần: Từ HOTP (RFC 4226) đến TOTP (RFC 6238)
Để hiểu rõ TOTP, trước tiên cần xem xét tiền thân trực tiếp của nó: thuật toán Mật khẩu Một lần Dựa trên HMAC (HOTP), được IETF công bố trong RFC 4226. HOTP được thiết kế xoay quanh một bộ đếm sự kiện gia tăng. Trong một giao dịch HOTP, cả trình tạo token phía client và máy chủ xác thực đều chia sẻ một khóa bí mật và duy trì một biến bộ đếm 64-bit nội bộ .
Mỗi khi người dùng yêu cầu một mã xác thực ở phía client, bộ đếm cục bộ sẽ tăng thêm 1 (), và một chuỗi băm mật mã mới được tính toán. Khi người dùng gửi mã kết quả lên, máy chủ xác thực sẽ tính toán giá trị kỳ vọng bằng bộ đếm đang lưu trữ. Nếu các giá trị trùng khớp, máy chủ sẽ tăng và cấp quyền truy cập.
Những Lỗ hổng Cố hữu của Bộ đếm Dựa trên Sự kiện
Mặc dù vững chắc về mặt toán học, HOTP lại gây ra sự bất tiện vận hành đáng kể trong các hệ thống phân tán:
- Mất đồng bộ Bộ đếm: Nếu người dùng kích hoạt token phần cứng hoặc chạm vào giao diện ứng dụng xác thực nhưng không gửi mã đã tạo, bộ đếm phía client sẽ tăng lên trong khi bộ đếm phía máy chủ vẫn đứng yên. RFC 4226 giải quyết vấn đề này bằng cách yêu cầu máy chủ triển khai một tham số cửa sổ nhìn trước (). Máy chủ phải tính toán và so sánh các giá trị trong phạm vi . Nếu quá nhỏ, người dùng vô tình kích hoạt token sẽ bị khóa tài khoản. Nếu quá lớn, bề mặt tấn công brute-force sẽ mở rộng tuyến tính.
- Chi phí Lưu trữ Trạng thái: Máy chủ xác thực phải duy trì trạng thái bộ đếm có thể thay đổi và đồng bộ xuyên suốt các bản sao cơ sở dữ liệu phân tán. Các cụm xác thực có độ đồng thời cao đòi hỏi kỹ thuật khóa hàng giao dịch để ngăn chặn điều kiện tranh định (race conditions) và các nỗ lực tấn công phát lại (replay attack) token.
Phép Chuyển đổi Bước Thời gian theo RFC 6238
RFC 6238 giải quyết bài toán mất đồng bộ bằng cách thay thế bộ đếm sự kiện gia tăng bằng một số nguyên tăng đơn điệu được trích xuất trực tiếp từ thời gian vật lý. Tọa độ thời gian vật lý liên tục được rời rạc hóa thành các bước thời gian riêng biệt bằng phép chia lấy sàn (floor division).
Bộ đếm rời rạc được tính toán chính thức theo công thức:
Trong đó:
- đại diện cho số giây hiện tại đã trôi qua kể từ Unix Epoch (1970-01-01T00:00:00Z UTC), bỏ qua giây nhuận theo quy ước POSIX.
- biểu diễn độ lệch thời gian epoch ban đầu, được tiêu chuẩn hóa là 0.
- xác định thời lượng của cửa sổ bước thời gian tính bằng giây, được quy ước và chỉ định theo tiêu chuẩn là 30 giây (mặc dù các triển khai đôi khi hỗ trợ khoảng thời gian 60 giây).
Physical Timeline (Continuous UTC Seconds)
----------------------------------------------------------------------->
[ 1730548800 s ] [ 1730548830 s ] [ 1730548860 s ]
| | |
+--- Step Window T = 57684960 +--- Step Window T = 57684961 +--- Step T = 57684962
| | |
v v v
[ HMAC Compute: C=57684960 ] [ HMAC Compute: C=57684961 ] [ HMAC Compute: C=... ]
[ Dynamic Truncation ] [ Dynamic Truncation ] [ Dynamic Truncation ]
v v v
Token: "847291" Token: "392014" Token: "105928"Vì thời gian tự động trôi qua trên cả phần cứng client và cụm máy chủ xác thực, client và máy chủ luôn giữ được sự đồng bộ tự nhiên mà không cần trao đổi trạng thái. Bộ đếm đóng vai trò là tham số đầu vào 8-byte cho hàm HMAC cốt lõi, bắc cầu nối giữa các hàm băm mật mã bất biến theo thời gian và các thử thách xác thực mang tính tạm thời.
2. Công thức Toán học của Cơ chế Tạo Token Dựa trên HMAC
Cơ chế mật mã học cốt lõi của TOTP dựa trên cấu trúc Mã xác thực Thông điệp Dựa trên Hàm băm (HMAC) được định nghĩa trong RFC 2104. HMAC cung cấp một cơ chế để xác minh tính toàn vẹn của dữ liệu lẫn tính xác thực của thông điệp thông qua một hàm băm mật mã nền tảng , được đánh giá kết hợp với một khóa bí mật dùng chung .
RFC 6238 hỗ trợ gốc nhiều hàm băm nguyên thủy:
- HMAC-SHA-1: Quy chuẩn mặc định phổ biến rộng rãi. Bất chấp các lỗ hổng xung đột đã biết trong SHA-1 đối với chữ ký số, HMAC-SHA-1 vẫn an toàn về mặt mật mã trước các cuộc tấn công hàm giả ngẫu nhiên (PRF) vì xung đột không làm lộ khóa.
- HMAC-SHA-256: Được khuyến nghị cho các hồ sơ bảo mật doanh nghiệp yêu cầu mức độ đảm bảo cao nhằm chống lại các phân tích mật mã trong tương lai.
- HMAC-SHA-512: Sử dụng kiến trúc từ 64-bit tương thích với các thanh ghi vi xử lý 64-bit hiện đại.
Định dạng Bộ đếm ở Cấp độ Byte
Trước khi tính toán chuỗi băm thông điệp, số nguyên 64-bit phải được chuyển đổi thành một mảng 8-byte (64-bit) chính xác theo thứ tự byte big-endian (thứ tự byte mạng). Nếu , biểu diễn thập lục phân của nó là 0x00000000037043E0. Trong bộ nhớ, giá trị này phải được cấu trúc thành tám octet riêng biệt:
Sai sót về padding hoặc thứ tự byte (endianness) ở giai đoạn này chính là nguyên nhân lỗi phổ biến nhất trong các triển khai TOTP tùy biến.
Cơ chế Hoạt động Nội bộ của HMAC
Cấu trúc HMAC hoạt động trên các khối có kích thước cố định ( byte cho SHA-1 và SHA-256; byte cho SHA-512) và chiều dài đầu ra . Khóa bí mật dùng chung được chuẩn hóa: nếu dài hơn , nó sẽ được băm về độ dài ; nếu ngắn hơn, nó sẽ được chèn thêm các byte 0 (zero-padded) về phía bên phải cho đủ byte.
Hai mặt nạ byte hằng số được định nghĩa:
- Lớp đệm trong (inner pad): lặp lại lần.
- Lớp đệm ngoài (outer pad): lặp lại lần.
Chuỗi băm mật mã được tính toán thông qua cấu trúc băm lồng nhau:
Trong đó biểu thị phép toán bitwise XOR, và biểu thị phép nối byte.
+-----------------------------------+
| Secret Key K (Base32 Decoded) |
+-----------------------------------+
|
+---------------+---------------+
| |
v v
( K XOR ipad ) ( K XOR opad )
| |
v |
[ Concatenate with T ] |
| |
v |
Hash Function H |
| |
v v
[ Intermediate Hash ] ------> [ Concatenate with Outer ]
|
v
Hash Function H
|
v
HMAC Digest HS (20-64 Bytes)Chuỗi băm tạo ra có độ dài khớp với thuật toán băm đã chọn: chính xác 20 byte cho SHA-1, 32 byte cho SHA-256, và 64 byte cho SHA-512.
3. Thuật toán Cắt Động (Dynamic Truncation - DT)
Đầu ra chuỗi băm mật mã thô 20-byte không phù hợp để người dùng nhập thủ công trực tiếp. Thuật toán Dynamic Truncation, được chỉ định trong RFC 4226 Phần 5.3, trích xuất một cách có hệ thống một số nguyên không dấu 31-bit từ chuỗi băm theo cách phân bổ tính ngẫu nhiên đồng đều, đồng thời ngăn chặn các sự cố tràn số nguyên.
Trích xuất Độ lệch (Offset Extraction)
Giả sử đại diện cho mảng kết quả HMAC được đánh chỉ số từ 0 đến 19 (đối với SHA-1). Thuật toán sẽ kiểm tra bốn bit thấp nhất (low-order nibble) của byte cuối cùng, .
Bởi vì một nibble 4-bit trải dài các giá trị từ đến ( đến ), chắc chắn thỏa mãn điều kiện:
Đảm bảo toán học này xác nhận rằng việc đọc bốn byte liên tiếp bắt đầu tại (các chỉ số ) sẽ không bao giờ vượt quá chỉ số 19 (), loại bỏ hoàn toàn các ngoại lệ vượt quá giới hạn mảng (out-of-bounds).
Cấu trúc Số nguyên 31-Bit
Sử dụng đã trích xuất, bốn byte liền kề được ghép thành một số nguyên không dấu 32-bit thông qua các phép dịch bit big-endian. Điểm cốt yếu là bit có trọng số lớn nhất (MSB) của byte đầu tiên được che mặt nạ bằng thay vì .
Việc che MSB bằng sẽ đặt bit dấu về 0, đảm bảo rằng số nguyên luôn được xử lý như một số nguyên dương không dấu 31-bit trên mọi kiến trúc. Điều này loại bỏ hành vi không đồng nhất trong các môi trường lập trình thiếu kiểu số nguyên không dấu nguyên bản (chẳng hạn như các phiên bản JavaScript cũ hơn hoặc một số hệ quản trị SQL nhất định).
Thu gọn Phép chia Lấy dư (Modulo Reduction)
Cuối cùng, mã xác thực chữ số của người dùng được tạo ra bằng cách áp dụng số học modulo cho :
Đối với mã 6 chữ số tiêu chuẩn ():
Nếu số nguyên thu được nhỏ hơn , nó sẽ được thêm các số 0 ở bên trái để duy trì chính xác ký tự. Ví dụ: nếu , nó sẽ được định dạng thành "004921".
/**
* Constant-time dynamic truncation and TOTP code derivation in TypeScript
* compliant with RFC 4226 and RFC 6238.
*/
export function dynamicTruncate(hmacDigest: Uint8Array, digits: number = 6): string {
if (hmacDigest.length < 20) {
throw new Error("HMAC digest length must be at least 20 bytes.");
}
// Step 1: Determine the 4-bit offset from the final byte
const offset = hmacDigest[hmacDigest.length - 1] & 0x0f;
// Step 2: Extract a 31-bit big-endian unsigned integer
const binaryCode =
((hmacDigest[offset] & 0x7f) << 24) |
((hmacDigest[offset + 1] & 0xff) << 16) |
((hmacDigest[offset + 2] & 0xff) << 8) |
(hmacDigest[offset + 3] & 0xff);
// Step 3: Modulo reduction
const tokenModulo = 10 ** digits;
const tokenNumber = binaryCode % tokenModulo;
// Step 4: Pad with leading zeroes to enforce string length
return tokenNumber.toString().padStart(digits, "0");
}4. Xử lý Lệch Đồng hồ, Sai lệch Cửa sổ và Độ trễ Truyền tải
Trong các môi trường lý tưởng, đồng hồ phía client và đồng hồ phía máy chủ khớp nhau hoàn hảo đến từng mili giây. Tuy nhiên, trong các triển khai vận hành thực tế, các thiết bị vật lý luôn gặp phải tình trạng trôi dạt đồng hồ phần cứng, lỗi Giao thức Thời gian Mạng (NTP) và độ trễ truyền dữ liệu biến thiên trên các đường truyền di động.
Ma trận Xác thực và Dung sai Sai lệch
Nếu người dùng gửi mã xác thực ở giây thứ 29 của cửa sổ 30 giây, độ trễ mạng có thể khiến gói tin đến máy chủ ở giây thứ 31. Dưới cơ chế xác thực không khoan nhượng nghiêm ngặt (), nỗ lực xác thực sẽ thất bại, gây khó khăn lớn cho người dùng.
Để thích ứng với hiện tượng lệch thời gian và độ trễ, RFC 6238 khuyến nghị hỗ trợ một cửa sổ xác thực trong vận hành. Một cửa sổ xác thực có kích thước sẽ chỉ thị cho máy chủ tính toán các token kỳ vọng trên ba khoảng thời gian riêng biệt:
Client Submits Token
|
v
Time Step T - 1 Time Step T Time Step T + 1
[ 00s ---- 30s ] [ 30s ---- 60s ] [ 60s ---- 90s ]
+------------------+--------------------+------------------+
| Past Window | Current Window | Future Window |
| (Acceptable) | (Nominal) | (Acceptable) |
+------------------+--------------------+------------------+
\ | /
+------------------+-------------------+
|
Evaluated by Server MatrixViệc mở rộng cửa sổ lên mang lại phạm vi xác thực hiệu dụng kéo dài 90 giây. Mặc dù điều này giúp khắc phục độ trễ tức thời, nhưng việc nới rộng vượt quá 1 sẽ làm tăng đáng kể xác suất tấn công brute-force thành công. Đối với mã 6 chữ số, kẻ tấn công có tỷ lệ đoán đúng trong một lần thử; việc đánh giá ba cửa sổ sẽ nâng tỷ lệ thành công của mỗi lần đoán lên .
Bảo vệ Chống Tấn công Phát lại thông qua Lưu trữ Trạng thái Cache
Vì cửa sổ xác thực mở rộng chấp nhận các mã hợp lệ về mặt toán học từ 30 giây trước, một kẻ tấn công nghe lén có thể chặn token đã gửi và phát lại nó trước khi cửa sổ hết hạn.
RFC 6238 bắt buộc các máy chủ xác thực phải duy trì một bộ nhớ cache chống phát lại (replay cache). Khi một token khớp với bước , máy chủ phải ghi lại bộ giá trị vào một bộ nhớ cache giao dịch tốc độ cao (như kho lưu trữ key-value trên bộ nhớ RAM) với thời gian tồn tại (TTL) tương ứng giây.
Nếu một yêu cầu tiếp theo gửi lên một token trùng khớp với chính bước đó, máy chủ sẽ từ chối yêu cầu ngay lập tức:
Receive (Token, UserID)
|
+--> For each T in { T_current - W ... T_current + W }:
ExpectedToken = CalculateTOTP(UserSecret, T)
If ConstantTimeEquals(Token, ExpectedToken):
If CacheExists(Key: UserID + ":" + T):
Return REJECT_REPLAYED_TOKEN
Else:
WriteCache(Key: UserID + ":" + T, TTL: (2W + 1) * X)
Return ACCEPT_AUTHENTICATION
|
+--> Return REJECT_INVALID_TOKENGiới hạn Tần suất và Bài toán Chi phí Tấn công Brute-Force
Bởi vì toàn bộ không gian token chỉ bao gồm giá trị đối với mã 6 chữ số, các ứng dụng bắt buộc phải thực thi cơ chế giới hạn tần suất (rate limiting) nghiêm ngặt. Các máy chủ xác thực nên áp dụng thuật toán lùi thời gian theo cấp số nhân (exponential backoff) hoặc tạm thời khóa tài khoản sau ba đến năm lần thử thất bại liên tiếp, ngăn chặn các cuộc tấn công quét mã tự động trong cửa sổ 30 giây.
5. Thực thi TOTP An toàn trên Trình duyệt Không Lộ Dữ liệu Từ xa
Các tiện ích truyền thống dành cho lập trình viên thường triển khai công cụ gỡ lỗi mật mã bằng cách chuyển tiếp dữ liệu nhập của người dùng qua các API backend. Trong bối cảnh xác thực hai yếu tố, việc gửi các khóa seed lên máy chủ từ xa làm phá vỡ toàn bộ cam kết bảo mật của xác thực đa yếu tố: bất kỳ ai nắm giữ seed Base32 đều có thể tạo mã token hợp lệ vô thời hạn.
PetexSpace giải quyết thách thức này bằng quy trình thực thi hoàn toàn trên trình duyệt. Toàn bộ chuỗi mật mã (phân tích Base32, tính toán HMAC và dynamic truncation) được thực hiện trực tiếp bên trong luồng thực thi của tác nhân người dùng thông qua Web Cryptography API nguyên bản (window.crypto.subtle), loại bỏ hoàn toàn việc truyền tải qua mạng đến máy chủ từ xa.
IN-BROWSER EXECUTION ENVIRONMENT
+----------------------------------------------------------------+
| |
| +------------------------+ +--------------------------+ |
| | Base32 Encoded Seed | ---> | Zero-Allocation Decoder | |
| | "JBSWY3DPEHPK3PXP" | | Uint8Array Key Buffer | |
| +------------------------+ +--------------------------+ |
| | |
| v |
| +------------------------+ +--------------------------+ |
| | DOM HighRes Time | ---> | crypto.subtle.importKey | |
| | Floor Division (30s) | | "raw", HMAC-SHA-1 | |
| +------------------------+ +--------------------------+ |
| | | |
| v v |
| +------------------------+ +--------------------------+ |
| | 8-Byte Big-Endian | ---> | crypto.subtle.sign | |
| | ArrayBuffer Counter | | In-Memory Signing | |
| +------------------------+ +--------------------------+ |
| | |
| v |
| +--------------------------+ |
| | Dynamic Truncation | |
| | 6-Digit Output Token | |
| +--------------------------+ |
| |
| (No Network Sockets, No Analytics, No Persistent Seed Writes) |
+----------------------------------------------------------------+Triển khai Bằng Web Crypto API
Các môi trường trình duyệt hiện đại cung cấp các hàm mật mã học thời gian hằng số (constant-time) được triển khai bằng mã máy nguyên bản thông qua crypto.subtle. Đoạn mã dưới đây minh họa việc tạo mã TOTP tuân thủ tiêu chuẩn mà không truyền dữ liệu ra bên ngoài:
/**
* Generates an RFC 6238 compliant TOTP code locally inside the browser.
* Utilizes the Web Cryptography API for high-assurance HMAC computation.
*/
export async function generateInBrowserTOTP(
keyBytes: Uint8Array,
timeStepSeconds: number = 30,
digits: number = 6
): Promise<{ token: string; remainingSeconds: number }> {
// Compute step counter from local high-resolution clock
const epochSeconds = Math.floor(Date.now() / 1000);
const timeStep = Math.floor(epochSeconds / timeStepSeconds);
const remainingSeconds = timeStepSeconds - (epochSeconds % timeStepSeconds);
// Allocate 8-byte big-endian buffer for the counter
const counterBuffer = new ArrayBuffer(8);
const counterView = new DataView(counterBuffer);
// High 32 bits
counterView.setUint32(0, Math.floor(timeStep / 0x100000000), false);
// Low 32 bits
counterView.setUint32(4, timeStep & 0xffffffff, false);
// Import raw key into Web Crypto API context
const cryptoKey = await window.crypto.subtle.importKey(
"raw",
keyBytes,
{ name: "HMAC", hash: { name: "SHA-1" } },
false,
["sign"]
);
// Compute HMAC digest in memory
const signature = await window.crypto.subtle.sign("HMAC", cryptoKey, counterBuffer);
const digestBytes = new Uint8Array(signature);
// Apply RFC 4226 Dynamic Truncation
const token = dynamicTruncate(digestBytes, digits);
return { token, remainingSeconds };
}Mô hình hóa Mối đe dọa trong Ngữ cảnh Phía Client
Mặc dù việc thực thi ở phía client loại bỏ được các lỗ hổng ở tầng truyền tải và rủi ro xâm phạm máy chủ, kiến trúc mật mã trong trình duyệt lại làm nảy sinh các vector đe dọa riêng biệt cần được giảm thiểu chủ động:
- Quét Bộ nhớ và Chèn Mã Tiện ích Mở rộng: Các tiện ích mở rộng không đáng tin cậy hoạt động với quyền hạn rộng
<all_urls>có thể kiểm tra nội dung DOM hoặc can thiệp vào các hàm khởi tạo toàn cục. PetexSpace giảm thiểu điều này bằng cách không phản ánh các khóa bí mật thô lên DOM một cách lâu dài, chỉ giữ các buffer khóa đã giải mã bên trong phạm vi hàm tạm thời. - Quản lý Bộ nhớ Tạm thời: Các runtime có cơ chế thu gom rác tự động (garbage collection) như V8 không đảm bảo việc xóa sạch bộ nhớ ngay lập tức. Khi có thể, lập trình viên cần ghi đè các buffer mảng định kiểu bằng các mẫu số 0 (
keyBytes.fill(0)) ngay sau khi nhập khóa mã hóa vàocrypto.subtle. - Tấn công Cross-Site Scripting (XSS): Nếu một ứng dụng thực thi logic mật mã bị xâm phạm qua XSS, kẻ tấn công có thể chạy mã tùy ý bên trong ngữ cảnh thực thi của người dùng. Việc duy trì một Chính sách Bảo mật Nội dung (CSP) nghiêm ngặt nhằm vô hiệu hóa
unsafe-evalvà giới hạn việc thực thi script chỉ cho các hash hoặc nonce đáng tin cậy là điều tối quan trọng.
Bằng cách áp dụng các công cụ web bảo toàn quyền riêng tư hoạt động hoàn toàn bên trong sandbox cục bộ, các lập trình viên và kỹ sư bảo mật có thể kiểm thử các triển khai mật mã với sự an tâm tuyệt đối rằng thông tin khóa seed riêng tư luôn được cách ly an toàn ngay trên chính thiết bị của họ.