-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathotp2fa.js
145 lines (131 loc) · 4.98 KB
/
otp2fa.js
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
140
141
142
143
144
145
/**
* One-time password implementation for NodeJS and the browser.
* https://gist.github.com/flipeador/d70a8a08b9600116cc102b6f63e8519e
*
* Special thanks to ChatGPT for its incredible skills.
*/
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
/**
* Convert a number into an 8-byte buffer.
* @param {number} number
*/
export function intToBuffer(number) {
const buffer = new Uint8Array(8);
for (let i = 7; i >= 0; i--) {
buffer[i] = number & 0xFF;
number >>= 8;
}
return buffer;
}
/**
* Convert a base32 string into a buffer.
* @param {string} base32
*/
export function base32ToBuffer(base32) {
const bytes = []; let bits = 0; let value = 0;
for (const char of base32.replace(/[=]+$/, '')) {
const index = CHARS.indexOf(char.toUpperCase());
if (index === -1)
throw new Error(`Invalid character: ${char}`);
value = (value << 5) | index; bits += 5;
if (bits >= 8)
bytes.push((value >> (bits -= 8)) & 0xFF);
}
return new Uint8Array(bytes);
}
/**
* Pad an otp number with leading zeros.
* @param {number} otp
* @param {number} [digits=10]
* The number of trailing digits to get. \
* It should be a value between 6 and 10.
*/
export function padotp(otp, digits=10) {
return `${otp}`.padStart(digits, '0').slice(-digits);
}
/**
* Generates an HMAC-based one-time password (HOTP).
* @param {Uint8Array|string} secret
* The shared secret key used to generate the HMAC.
* @param {Uint8Array|number} counter
* The counter value, incremented each time an OTP is generated.
* @param {'SHA-1'|'SHA-256'|'SHA-512'} [algorithm='SHA-1']
* The hash algorithm used in the HMAC function.
* @returns {Promise<number>}
* A promise that resolves to a 31-bit integer representing the generated OTP.
*/
export async function hotp(secret, counter, algorithm='SHA-1') {
if (typeof(secret) === 'string')
secret = base32ToBuffer(secret);
if (typeof(counter) === 'number')
counter = intToBuffer(counter);
// Generate a signature using the HMAC algorithm.
// https://developer.mozilla.org/docs/Web/API/SubtleCrypto/sign
const buffer = new Uint8Array(
await crypto.subtle.sign(
'HMAC',
// Import the secret key to create a cryptographic key.
// https://developer.mozilla.org/docs/Web/API/SubtleCrypto/importKey
await crypto.subtle.importKey(
'raw', // the key material is in raw binary format
secret, // the secret used as the key for the HMAC
{ name: 'HMAC', hash: algorithm },
false, // the key is non-extractable
['sign'] // the key can only be used for signing
),
counter // the data to be signed
) // ArrayBuffer
);
// Extract the last 4 bits of the hash buffer as a decimal integer.
// The offset indicates where in the hash to start extracting the OTP.
const offset = buffer[buffer.length - 1] & 0xF;
// Extract 4 bytes starting from the calculated offset.
const otp = // dynamic truncation
buffer[offset] << 24 | buffer[offset + 1] << 16 |
buffer[offset + 2] << 8 | buffer[offset + 3];
// Clear the highest bit, ensuring the result is a 31-bit integer.
return otp & 0x7FFFFFFF;
}
/**
* Generates a time-based one-time password (TOTP).
* @param {Uint8Array} secret
* The shared secret key used to generate the HMAC.
* @param {number} time
* The Unix timestamp, in seconds.
* @param {number} [period=30]
* The period that the passcode will be valid for, in seconds.
* @param {'SHA-1'|'SHA-256'|'SHA-512'} [algorithm='SHA-1']
* The hash algorithm used in the HMAC function.
* @returns {Promise<number>}
* A promise that resolves to a 31-bit integer representing the generated OTP.
* @example
* const time = Math.floor(Date.now() / 1000);
* const code = await totp(generateSecret(), time, 30);
* console.log(padotp(code, 6), 30-time%30, 'seconds');
*/
export async function totp(secret, time, period=30, algorithm='SHA-1') {
const counter = Math.floor(time / period);
return await hotp(secret, counter, algorithm);
}
/**
* Generates a random base32 secret of the specified length.
* @param {number} [length=24]
*/
export function generateSecret(length=24) {
let secret = '';
while (length-- > 0)
secret += CHARS[Math.floor(Math.random() * CHARS.length)];
return secret;
}
/**
* Generates an OTP Auth URL, to be encoded in a QR code.
* @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format
*/
export function otpauthURL(issuer, label, secret, params={}) {
issuer = encodeURIComponent(issuer); label = encodeURIComponent(label);
const url = new URL(`otpauth://${params.type??'totp'}/${issuer}:${label}`);
url.searchParams.set('issuer', issuer); url.searchParams.set('secret', secret);
for (const key of ['algorithm', 'digits', 'period', 'counter'])
if (params[key]) url.searchParams.set(key, params[key]);
return url.toString();
}