简单解析TOTP算法

简单解析 TOTP 算法

什么是 TOTP？

TOTP，全称：基于时间的一次性密码（Time-based one-time password），该算法遵循RFC 6238标准，在遵循 RFC 4226 标准的 HOTP 算法的基础上，附带了时间作为消息的增量计数器。

TOTP 的应用在哪里？

最直观的体现就是Microsoft Authenticator和Google 推出的 Authenticator应用，可以通过扫描二维码或者输入密钥（Secret）来添加对应的账号。

如何实现的？

首先，假设我们已经拥有了一串可用于生成密钥的 BASE32 字符串：4FCDTLHR446DPFCKUA46UFIAYTQIDSZ2
将这个字符串转换为字节码：

Copy
34 46 43 44 54
4c 48 52 34 34
36 44 50 46 43
4b 55 41 34 36
55 46 49 41 59
54 51 49 44 53
5a 32

然后对其进行 BASE32 解码，就可以得到 20 个十六进制序列：

Copy
0xE1 0x44 0x39 0xAC 0xF1
0xE7 0x3C 0x37 0x94 0x4A
0xA0 0x39 0xEA 0x15 0x00
0xC4 0xE0 0x81 0xCB 0x3A

然后以今天中午 12 时作为时间因素：2023 年 06 月 26 日 12:00:00（时间戳：1687752000（秒））
有了以上 2 个因素，我们就可以开始计算了

第二步，我们将时间因素的时间戳除以 30 作为冗余（30 秒窗口期）：1687752000 / 30 = 56258400，然后再将其转换为十六进制：0x035A6F60

注意：在此需要给补 0 到 8 个字节的长度：00 00 00 00 03 5A 6F 60

第三步，将上文的私钥作为 HMAC-SHA-1 的私钥，将得到的时间因素的结果作为消息进行加密，然后得到了：

Copy
ee 91 c2 ea 63
f2 e4 9c 65 5d
c5 2e c4 9f b0
dd 45 f6 09 09

第四步，将得到的哈希的最后一位并按位与上 0x0F 作为偏移量（OFFSET）：0x09 & 0x0F = 0x09
将偏移量带入如下算式中：

Copy
哈希结果为HASH
偏移量为OFFSET = 0x09

(HASH[OFFSET] & 0x7F) << 24
= (0x5D & 0x7F) << 24
= 0x5D << 24
= 0x5D000000

(HASH[OFFSET + 1] & 0xFF) << 16
= (0xC5 & 0xFF) << 16
= 0xC5 << 16
= 0xC50000

(HASH[OFFSET + 2] & 0xFF) << 8
= (0x2E & 0xFF) << 8
= 0x2E << 8
= 0x2E00

HASH[OFFSET + 3] & 0xFF
= 0xC4 & 0xFF
= 0xC4

将上述结果进行按位或运算，并取后6位（十进制）
(0x5D000000 | 0xC50000 | 0x2E00 | 0xC4) % 0xF4240 = 0x5DC52EC4 % 0xF4240
= 1573203652 % 1000000 = 203652

至此，203652 就是我们生成的 TOTP 验证码。

代码（C 语言）

注：需要引入 OpenSSL

Copy
#include <stdio.h>
#include "string.h"
#include "time.h"
#include "math.h"
#include "openssl/hmac.h"

void generate_totp_code(const uint8_t * secret, uint32_t counter, int digits) {
uint8_t counter_bytes[8];
for (int i = 1; i <= 8; ++i) {
counter_bytes[8 - i] = (u_int8_t) (counter % 256) & 0xff;
counter /= 256;
}
unsigned char hmac_result[20];
HMAC(EVP_sha1(), secret, 20, counter_bytes,
sizeof(counter_bytes), hmac_result, NULL);
int offset = hmac_result[19] & 0x0f;
int truncated_hash = (hmac_result[offset] & 0x7f) << 24 |
(hmac_result[offset + 1] & 0xff) << 16 |
(hmac_result[offset + 2] & 0xff) << 8 |
(hmac_result[offset + 3] & 0xff);
int otp = truncated_hash % 0xF4240;
printf("TOTP: %0*d\n", digits, otp);
}

int main() {
// 4FCDTLHR446DPFCKUA46UFIAYTQIDSZ2
// to bytes
// 34 46 43 44 54 4c 48 52 34 34 36 44 50 46 43 4b 55 41 34 36 55 46 49 41 59 54 51 49 44 53 5a 32
// base32 decode
// e1 44 39 ac f1 e7 3c 37 94 4a a0 39 ea 15 00 c4 e0 81 cb 3a
uint8_t secret[] = {0xe1, 0x44, 0x39, 0xac,
0xf1, 0xe7, 0x3c, 0x37,
0x94, 0x4a, 0xa0, 0x39,
0xea, 0x15, 0x00, 0xc4,
0xe0, 0x81, 0xcb, 0x3a};
uint32_t counter = time(NULL) / 30;
int digits = 6;
generate_totp_code(secret, counter, digits);
return 0;
}