1const secret = document.getElementById('secret');
2const period = document.getElementById('period');
3const digits = document.getElementById('digits');
4const circle = document.querySelector("circle");
5const perimeter = circle.getAttribute("r") * 2 * Math.PI;
6
7function base32ToBytes(base32) {
8 const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
9 let bits = '';
10 for (const char of base32) {
11 const val = base32Chars.indexOf(char.toUpperCase());
12 if (val === -1) {
13 throw new Error("Invalid Base32 character");
14 }
15 bits += val.toString(2).padStart(5, '0');
16 }
17 const bytes = [];
18 for (let i = 0; i < bits.length; i += 8) {
19 const byte = bits.substring(i, i + 8);
20 if (byte.length === 8) {
21 bytes.push(parseInt(byte, 2));
22 }
23 }
24 return new Uint8Array(bytes);
25}
26
27// Helper function to convert integer to byte array
28function intToBytes(num) {
29 const bytes = new Uint8Array(8);
30 for (let i = 7; i >= 0; i--) {
31 bytes[i] = num & 0xff;
32 num = num >> 8;
33 }
34 return bytes;
35}
36
37// HMAC-SHA1 function using Web Crypto API
38async function hmacSha1(key, message) {
39 const cryptoKey = await crypto.subtle.importKey(
40 'raw',
41 key,
42 { name: 'HMAC', hash: 'SHA-1' },
43 false,
44 ['sign']
45 );
46 const signature = await crypto.subtle.sign('HMAC', cryptoKey, message);
47 return new Uint8Array(signature);
48}
49
50async function generateTOTP({
51 seed,
52 timePeriod,
53 length
54}) {
55 const key = base32ToBytes(seed);
56 const epoch = Math.floor(Date.now() / 1000);
57 const time = Math.floor(epoch / timePeriod);
58 const timeBytes = intToBytes(time);
59 const hmac = await hmacSha1(key, timeBytes);
60
61 const offset = hmac[hmac.length - 1] & 0x0f;
62 const binary = (
63 ((hmac[offset] & 0x7f) << 24) |
64 ((hmac[offset + 1] & 0xff) << 16) |
65 ((hmac[offset + 2] & 0xff) << 8) |
66 (hmac[offset + 3] & 0xff)
67 );
68
69 const otp = binary % 10 ** length;
70 return otp.toString().padStart(length, '0');
71}
72
73async function setTOTP() {
74 const timeRemaining = (period.value - (Date.now() / 1000) % period.value).toFixed(0);
75 if (timeRemaining <= 0) {
76 return;
77 }
78 const otp = await generateTOTP({
79 seed: secret.value,
80 timePeriod: period.value,
81 length: digits.value
82 });
83 document.getElementById('otp').innerText = otp;
84 document.getElementBy
85}
86
87secret.addEventListener('input', async (e) => {
88 e.target.value = e.target.value.toUpperCase();
89 await setTOTP();
90});
91
92period.addEventListener('input', async (e) => {
93 if (e.target.value < 3) {
94 e.target.value = 3;
95 }
96 await setTOTP();
97});
98
99digits.addEventListener('input', async (e) => {
100 if (e.target.value < 1) {
101 e.target.value = 1;
102 }
103 if (e.target.value > 10) {
104 e.target.value = 10;
105 }
106 await setTOTP();
107});
108
109function updateCircle() {
110 const timeRemaining = (period.value - (Date.now() / 1000) % period.value).toFixed(0);
111 circle.setAttribute(
112 "stroke-dashoffset",
113 (perimeter * timeRemaining) / period.value - perimeter
114 );
115 document.getElementById("remaining").innerText = `Remaining: ${timeRemaining}s`;
116}
117
118updateCircle()
119setTOTP()
120setInterval(() => { updateCircle(); setTOTP() }, 1000);