Back to Home

Khám phá Timebased OTP

Bạn đã bao giờ tự hỏi Google Authenticator hoạt động như thế nào chưa? Hoặc từng thắc mắc làm thế nào ngân hàng có thể sử dụng token để tạo mật khẩu (mật khẩu này thay đổi liên tục theo thời gian) và người dùng có thể sử dụng mật khẩu này để đăng nhập vào hệ thống ngân hàng. Điều bí ẩn là token này là một thiết bị cực nhỏ, tiêu thụ rất ít điện năng và không có kết nối internet, làm sao thiết bị này có thể tạo ra mật khẩu có thể được xác thực bởi hệ thống máy chủ ngân hàng (làm sao ngân hàng có thể xác thực được loại mật khẩu này)?

Trong bài viết này, mình sẽ giải thích cơ chế hoạt động của Timebased OTP, cũng là nguyên tắc cốt lõi của dạng mật khẩu hiện thực hóa các hệ thống 2FA (Two Factor Authentication). Tuy nhiên để đơn giản hóa, mình sẽ giản lược bớt các chi tiết kỹ thuật chi tiết mà chỉ chú trọng vào luồng chính để các bạn tiện hình dung và theo dõi cho nên sẽ không match 100% so với thực tế.

Một liên tưởng ẩn dụ

Hãy tưởng tượng bạn cầm một chiếc chìa khóa hình trụ. Trên thân nó có một cửa sổ nhỏ chỉ hiển thị được 6 con số.

Cấu tạo lõi bí mật (Shared Secret): Bên trong lớp vỏ thép là 6 bánh răng xếp trục dọc. Điều đặc biệt là trên bề mặt mỗi bánh răng, các con số từ 0-9 không được in theo thứ tự 0, 1, 2... mà được sắp xếp theo một thứ tự hỗn loạn cực kỳ riêng biệt (ví dụ: 7, 2, 9, 0...). Thứ tự này chính là "bí mật" mà chỉ cái chìa của bạnổ khóa ở máy chủ sở hữu.

  • Nhịp tự động (Time Step): Bên trong chìa khóa có một bộ đếm giờ bằng dây cót. Cứ đúng 30 giây, một lẫy thép bên trong sẽ "tạch" một tiếng, đẩy tất cả các bánh răng xoay đi đúng 1 nấc.

  • Cửa sổ hiển thị (The Output): Tại mỗi nấc xoay, một tổ hợp 6 con số mới sẽ xuất hiện ở cửa sổ nhỏ.

    • Vì các con số trên bánh răng được sắp xếp lộn xộn, nên mã số nhảy từ 112 456 sang 890 123 nhìn có vẻ hoàn toàn ngẫu nhiên.

    • Nhưng thực tế, nó tuân theo một quy luật cơ khí tuyệt đối dựa trên vị trí bắt đầusố lần "tạch" của bánh răng.

  • Xác thực không dây: Khi bạn nhập 6 số này vào hệ thống, máy chủ không cần gọi điện cho bạn. Nó chỉ cần nhìn vào "chiếc chìa ảo" của nó, tính toán xem sau bao nhiêu nấc xoay (tính từ lúc khởi tạo) thì các bánh răng của nó đang hiện ra số gì. Nếu hai con số khớp nhau, cửa sẽ mở.

Sơ đồ hoạt động đơn giản

Dưới đây là một mô tả sơ đồ hoạt động đơn giản (nhưng vẫn giữ tinh thần chủ đạo) để các bạn dễ theo dõi.

Mô tả

  • Máy chủ tạo số SS ngẫu nhiên cho mỗi người dùng.

  • Người dùng sẽ lưu trữ SS trong ứng dụng di động của mình dưới dạng giá trị bí mật.

    • Đối với Google Authenticator, việc này được thực hiện thông qua việc quét QRCode

    • Đối với token ngân hàng, số này sẽ được thiết lập cho từng người dùng cụ thể mỗi lần cấp phát cho người dùng.

  • Trên thiết bị của khách hàng (Mobile App hoặc token) sẽ có đồng hồ được kích hoạt và máy chủ sẽ ghi nhận thời gian này là T0T_0.

  • Sau mỗi khoảng thời gian TT (thông thường là 30 giây), mật khẩu mới PxP_x sẽ được tạo

Demo

Chi tiết thuật toán trong Demo

Quy trình tạo password PxP_x (Lưu ý rằng các tính toán ở đây được mình cố tình chọn các số đơn giản, trong thực tế thì không đơn giản như vậy để tăng tính bảo mật)

Tại thời điểm TxT_x, một password PxP_x sẽ được tạo ra nhờ vào giá trị bí mật SS và một biến đếm thời gian CxC_x. CxC_x sẽ được update tăng dần theo mỗi khoảng thời gian TTthường là 15s-30s, và được tính như sau: Cx=TxT0T\displaystyle Cx = \frac {T_x - T_0} T

Lúc đó, PxP_x sẽ được tính bởi: Px=SCxmod999,983P_x = S^{C_x} \mod 999,983. Lưu ý là giá trị 999,983999,983 là một số được chọn để đảm bảo password được tạo ra giới hạn giá trị giới hạn trong 6 chữ số. (again, đây là một công thức mình dùng để đơn giản hóa tính toán chứ không phải dùng trong thực tế nhé)

🔐 Thuật toán TOTP trong thực tế

Trong thực tế, TOTP không phải là một “ý tưởng mơ hồ”, mà được chuẩn hoá rất rõ ràng trong IETF RFC 6238.

TOTP thực chất được xây dựng từ HOTP (RFC 4226), với điểm khác biệt duy nhất:

Thay vì dùng counter, TOTP dùng thời gian.

Công thức cốt lõi

Ta định nghĩa:

T=currentTimeT0XT = \left\lfloor \frac{\text{currentTime} - T_0}{X} \right\rfloor

Trong đó:

  • currentTime\text{currentTime}: thời gian hiện tại (Unix time)

  • T0T_0: Mốc thời gian (thường = 0)

  • XX: Bước thời gian (thường = 30 giây)

Sau đó ta tính TOTPTOTP:

TOTP=HOTP(secret,T)TOTP = HOTP(\text{secret}, T)

Với:

  • HOTPHOTP là một hàm hash (thường là SHA-1, nhưng có thể là SHA-256 hoặc SHA-512).

  • secret\text{secret} là một shared secret giữ client và server

Bước 1: Tạo HMAC

HMAC=HMACSHA1(K,T)HMAC=HMAC-SHA1(K,T)

Bước 2: Dynamic truncation

Lấy 4 byte từ HMAC (theo offset từ byte cuối):

  • offset = last byte & 0x0F

  • lấy 4 byte từ vị trí đó

👉 chuyển thành số nguyên 31-bit

Bước 3: Sinh OTP

0x2dx\int_0^\infty x^2 dx

Với dd là số chữ số (thường là 6)

Pseudo code:

uint32_t generate_totp(
    const std::vector<uint8_t>& secret,
    uint64_t current_time,
    uint32_t step = 30,
    uint32_t digits = 6
) {
    // Bước 1: Tính counter từ thời gian
    uint64_t T = current_time / step;

    // Convert T thành big-endian 8 bytes
    std::vector<uint8_t> msg(8);
    for (int i = 7; i >= 0; --i) {
        msg[i] = T & 0xFF;
        T >>= 8;
    }

    // Bước 2: HMAC
    std::vector<uint8_t> hmac = hmac_sha1(secret, msg);

    // Bước 3: Dynamic truncation
    uint32_t binary = dynamic_truncate(hmac);

    // Bước 4: Modulo để lấy số chữ số mong muốn
    uint32_t otp = binary % static_cast<uint32_t>(pow(10, digits));

    return otp;
}

⚠️ Những vấn đề thực tế cần xử lý

  • Clock drift: Client và server có thể lệch vài giây. Giải pháp là server sẽ chấp nhận ±1\pm1 step.

  • Secret cần phải được bảo vệ tuyệt đối vì nếu lộ thì attacker có thể sinh chuỗi TOTP vô hạn.

  • Phising real-time: Attacker có thể lừa bạn nhập OTP, sau đó chúng lấy đó để dùng ngay lập tức. Vì thế, TOTP đang dần bị thay thế bởi WebAuthn hoặc Passkeys.

Comments

0/300

Leave name/email blank to comment anonymously

No comments yet. Be the first to comment!