main
90e35a3 ยท 1 year ago 22 commits
  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);