Skip to content

Commit c7a1e51

Browse files
committed
chore: 新增 generateTOTP 函數
1 parent d68f59a commit c7a1e51

1 file changed

Lines changed: 84 additions & 0 deletions

File tree

src/utils/generateTOTP.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
export async function generateTOTP(secret: string, digits = 6, period = 30): Promise<string> {
2+
// 1. 計算 counter
3+
const counter = Math.floor(Date.now() / 1000 / period);
4+
5+
// 2. secret(base32) -> bytes
6+
const key = base32Decode(secret);
7+
8+
// 3. counter -> 8-byte big-endian
9+
const counterBuffer = new ArrayBuffer(8);
10+
const counterView = new DataView(counterBuffer);
11+
12+
// 高 32 bits
13+
counterView.setUint32(0, Math.floor(counter / 0x100000000));
14+
15+
// 低 32 bits
16+
counterView.setUint32(4, counter >>> 0);
17+
18+
// 4. 建立 HMAC key
19+
const cryptoKey = await crypto.subtle.importKey(
20+
"raw",
21+
key,
22+
{
23+
name: "HMAC",
24+
hash: "SHA-1", // Google Authenticator 標準
25+
},
26+
false,
27+
["sign"],
28+
);
29+
30+
// 5. HMAC(counter)
31+
const hmac = await crypto.subtle.sign("HMAC", cryptoKey, counterBuffer);
32+
33+
const hmacBytes = new Uint8Array(hmac);
34+
35+
// 6. Dynamic truncation
36+
const offset = hmacBytes[hmacBytes.length - 1] & 0xf;
37+
38+
const binary =
39+
((hmacBytes[offset] & 0x7f) << 24) |
40+
((hmacBytes[offset + 1] & 0xff) << 16) |
41+
((hmacBytes[offset + 2] & 0xff) << 8) |
42+
(hmacBytes[offset + 3] & 0xff);
43+
44+
// 7. 取 digits 位數
45+
const otp = binary % 10 ** digits;
46+
47+
return otp.toString().padStart(digits, "0");
48+
}
49+
50+
/**
51+
* Base32 解碼
52+
* RFC4648
53+
*/
54+
function base32Decode(base32: string): ArrayBuffer {
55+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
56+
57+
let bits = "";
58+
let result: number[] = [];
59+
60+
base32 = base32.replace(/=+$/, "").toUpperCase();
61+
62+
for (const char of base32) {
63+
const val = alphabet.indexOf(char);
64+
65+
if (val === -1) {
66+
throw new Error(`Invalid base32 char: ${char}`);
67+
}
68+
69+
bits += val.toString(2).padStart(5, "0");
70+
}
71+
72+
for (let i = 0; i + 8 <= bits.length; i += 8) {
73+
result.push(parseInt(bits.slice(i, i + 8), 2));
74+
}
75+
76+
const buffer = new ArrayBuffer(result.length);
77+
const bytes = new Uint8Array(buffer);
78+
79+
for (let i = 0; i < result.length; i++) {
80+
bytes[i] = result[i];
81+
}
82+
83+
return buffer;
84+
}

0 commit comments

Comments
 (0)