-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtotp.js
More file actions
140 lines (120 loc) · 4.38 KB
/
Copy pathtotp.js
File metadata and controls
140 lines (120 loc) · 4.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/**
* totp.js – TOTP code generation per RFC 6238 (HMAC-SHA1, 30 s, 6 digits by default).
*
* Uses Web Crypto API for HMAC; no external libraries required.
*/
const DEFAULT_TOTP_PERIOD = 30; // seconds
const DEFAULT_TOTP_DIGITS = 6;
const DEFAULT_TOTP_ALGO = "SHA-1";
/* ------------------------------------------------------------------ */
/* Base32 helpers */
/* ------------------------------------------------------------------ */
const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
/**
* Validate that a string is legal Base32 (RFC 4648).
*/
export function isValidBase32(str) {
if (!str || str.length === 0) return false;
const cleaned = str.replace(/[\s=-]/g, "").toUpperCase();
return /^[A-Z2-7]+$/.test(cleaned);
}
/**
* Decode a Base32 string into a Uint8Array.
*/
export function base32ToBytes(base32) {
const cleaned = base32.replace(/[\s=-]/g, "").toUpperCase();
let bits = "";
for (const ch of cleaned) {
const val = BASE32_ALPHABET.indexOf(ch);
if (val === -1) throw new Error("Invalid Base32 character: " + ch);
bits += val.toString(2).padStart(5, "0");
}
const bytes = [];
for (let i = 0; i + 8 <= bits.length; i += 8) {
bytes.push(parseInt(bits.substring(i, i + 8), 2));
}
return new Uint8Array(bytes);
}
/* ------------------------------------------------------------------ */
/* TOTP core */
/* ------------------------------------------------------------------ */
/**
* Generate the current TOTP code for a given secret (plaintext Base32).
*/
export async function generateTOTP(secretBase32, period = DEFAULT_TOTP_PERIOD, digits = DEFAULT_TOTP_DIGITS, algorithm = DEFAULT_TOTP_ALGO) {
const keyBytes = base32ToBytes(secretBase32);
const epoch = Math.floor(Date.now() / 1000);
const counter = Math.floor(epoch / period);
// Convert counter to 8-byte big-endian buffer
const counterBuf = new ArrayBuffer(8);
const view = new DataView(counterBuf);
const high = Math.floor(counter / 0x100000000);
const low = counter % 0x100000000;
view.setUint32(0, high, false);
view.setUint32(4, low, false);
let hashName = algorithm.toUpperCase();
if (hashName === "SHA1") hashName = "SHA-1";
if (hashName === "SHA256") hashName = "SHA-256";
if (hashName === "SHA512") hashName = "SHA-512";
const cryptoKey = await crypto.subtle.importKey(
"raw",
keyBytes,
{ name: "HMAC", hash: hashName },
false,
["sign"]
);
const hmac = new Uint8Array(
await crypto.subtle.sign("HMAC", cryptoKey, counterBuf)
);
// Dynamic truncation (RFC 4226 §5.4)
const offset = hmac[hmac.length - 1] & 0x0f;
const binary =
((hmac[offset] & 0x7f) << 24) |
((hmac[offset + 1] & 0xff) << 16) |
((hmac[offset + 2] & 0xff) << 8) |
(hmac[offset + 3] & 0xff);
const otp = binary % Math.pow(10, digits);
return otp.toString().padStart(digits, "0");
}
/**
* Seconds remaining until the next TOTP refresh.
*/
export function secondsRemaining(period = DEFAULT_TOTP_PERIOD) {
return period - (Math.floor(Date.now() / 1000) % period);
}
/**
* Generate a HOTP code for a given secret and counter (RFC 4226).
*/
export async function generateHOTP(secretBase32, counter, digits = DEFAULT_TOTP_DIGITS, algorithm = DEFAULT_TOTP_ALGO) {
const keyBytes = base32ToBytes(secretBase32);
// Convert counter to 8-byte big-endian buffer
const counterBuf = new ArrayBuffer(8);
const view = new DataView(counterBuf);
const high = Math.floor(counter / 0x100000000);
const low = counter % 0x100000000;
view.setUint32(0, high, false);
view.setUint32(4, low, false);
let hashName = algorithm.toUpperCase();
if (hashName === "SHA1") hashName = "SHA-1";
if (hashName === "SHA256") hashName = "SHA-256";
if (hashName === "SHA512") hashName = "SHA-512";
const cryptoKey = await crypto.subtle.importKey(
"raw",
keyBytes,
{ name: "HMAC", hash: hashName },
false,
["sign"]
);
const hmac = new Uint8Array(
await crypto.subtle.sign("HMAC", cryptoKey, counterBuf)
);
// Dynamic truncation (RFC 4226 §5.4)
const offset = hmac[hmac.length - 1] & 0x0f;
const binary =
((hmac[offset] & 0x7f) << 24) |
((hmac[offset + 1] & 0xff) << 16) |
((hmac[offset + 2] & 0xff) << 8) |
(hmac[offset + 3] & 0xff);
const otp = binary % Math.pow(10, digits);
return otp.toString().padStart(digits, "0");
}