mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-06-15 05:22:44 +01:00
59 lines
2.0 KiB
TypeScript
59 lines
2.0 KiB
TypeScript
import { createHmac } from 'node:crypto';
|
|
|
|
const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
|
|
function base32Decode(input: string): Buffer {
|
|
const normalized = input
|
|
.toUpperCase()
|
|
.replace(/=+$/, '')
|
|
.replace(/[^A-Z2-7]/g, '');
|
|
let bits = 0;
|
|
let value = 0;
|
|
const bytes: number[] = [];
|
|
for (const char of normalized) {
|
|
value = (value << 5) | BASE32_ALPHABET.indexOf(char);
|
|
bits += 5;
|
|
if (bits >= 8) {
|
|
bytes.push((value >>> (bits - 8)) & 0xff);
|
|
bits -= 8;
|
|
}
|
|
}
|
|
return Buffer.from(bytes);
|
|
}
|
|
|
|
/**
|
|
* Generates a 6-digit TOTP code (RFC 6238, SHA-1, 30 second period) for the
|
|
* given base32 secret — the "Setup Key" shown while enabling 2FA.
|
|
*/
|
|
export function generateTotpCode(base32Secret: string, atMs: number = Date.now()): string {
|
|
const counter = Math.floor(atMs / 1000 / 30);
|
|
const counterBuffer = Buffer.alloc(8);
|
|
counterBuffer.writeBigUInt64BE(BigInt(counter));
|
|
const digest = createHmac('sha1', base32Decode(base32Secret)).update(counterBuffer).digest();
|
|
const offset = digest[digest.length - 1] & 0x0f;
|
|
const code =
|
|
((digest[offset] & 0x7f) << 24) |
|
|
((digest[offset + 1] & 0xff) << 16) |
|
|
((digest[offset + 2] & 0xff) << 8) |
|
|
(digest[offset + 3] & 0xff);
|
|
return (code % 1_000_000).toString().padStart(6, '0');
|
|
}
|
|
|
|
/**
|
|
* Generates a syntactically valid TOTP code that is guaranteed to be rejected,
|
|
* by using a timestamp far outside the accepted verification window.
|
|
*/
|
|
export function generateInvalidTotpCode(base32Secret: string): string {
|
|
const validNow = [
|
|
generateTotpCode(base32Secret, Date.now() - 30_000),
|
|
generateTotpCode(base32Secret),
|
|
generateTotpCode(base32Secret, Date.now() + 30_000),
|
|
];
|
|
for (let minutes = 10; ; minutes++) {
|
|
const candidate = generateTotpCode(base32Secret, Date.now() + minutes * 60_000);
|
|
if (!validNow.includes(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
}
|