This repository has been archived by the owner on Jun 19, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathindex.ts
60 lines (54 loc) · 2.44 KB
/
index.ts
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
/*! micro-otp - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { hmac } from '@noble/hashes/hmac';
import { sha1 } from '@noble/hashes/sha1';
import { sha256, sha512 } from '@noble/hashes/sha2';
import { base32 } from '@scure/base';
import { U32BE, U64BE } from 'micro-packed';
export type Bytes = Uint8Array;
export type OTPOpts = { algorithm: string; digits: number; interval: number; secret: Bytes };
function parseSecret(secret: string) {
return base32.decode(secret.padEnd(Math.ceil(secret.length / 8) * 8, '=').toUpperCase());
}
export function parse(otp: string): OTPOpts {
const opts = { secret: new Uint8Array(), algorithm: 'sha1', digits: 6, interval: 30 };
if (otp.startsWith('otpauth://totp/')) {
// @ts-ignore
if (typeof URL === 'undefined') throw new Error('global variable URL must be defined');
// @ts-ignore
const url = new URL(otp);
if (url.protocol !== 'otpauth:' || url.host !== 'totp') throw new Error('OTP: wrong url');
const params = url.searchParams;
const digits = params.get('digits');
if (digits) {
opts.digits = +digits;
if (!['6', '7', '8'].includes(digits))
throw new Error(`OTP: url should include 6, 7 or 8 digits. Got: ${digits}`);
}
const algo = params.get('algorithm');
if (algo) {
if (!['sha1', 'sha256', 'sha512'].includes(algo.toLowerCase()))
throw new Error(`OTP: url with wrong algorithm: ${algo}`);
opts.algorithm = algo.toLowerCase();
}
const secret = params.get('secret');
if (!secret) throw new Error('OTP: url without secret');
opts.secret = parseSecret(secret);
} else opts.secret = parseSecret(otp);
return opts;
}
export function buildURL(opts: OTPOpts): string {
return `otpauth://totp/?secret=${base32.encode(opts.secret).replace(/=/gm, '')}&interval=${
opts.interval
}&digits=${opts.digits}&algorithm=${opts.algorithm.toUpperCase()}`;
}
export function hotp(opts: OTPOpts, counter: number | bigint) {
const hash = { sha1, sha256, sha512 }[opts.algorithm];
if (!hash) throw new Error(`TOTP: unknown hash: ${opts.algorithm}`);
const mac = hmac(hash, opts.secret, U64BE.encode(BigInt(counter)));
const offset = mac[mac.length - 1]! & 0x0f;
const num = U32BE.decode(mac.slice(offset, offset + 4)) & 0x7fffffff;
return num.toString().slice(-opts.digits).padStart(opts.digits, '0');
}
export function totp(opts: OTPOpts, ts = Date.now()) {
return hotp(opts, Math.floor(ts / (opts.interval * 1000)));
}