/* * vault1984 — TOTP generation (RFC 6238) * Runs in both QuickJS (CLI) and browser (extension). * * In CLI (QuickJS): native_hmac_sha1 provided by jsbridge.c via BearSSL. * All calls are synchronous. * In browser: Web Crypto API (async). */ /* IS_BROWSER defined in crypto.js, reuse if available */ var IS_BROWSER_TOTP = (typeof IS_BROWSER !== 'undefined') ? IS_BROWSER : (typeof globalThis.crypto !== 'undefined' && typeof globalThis.crypto.subtle !== 'undefined'); /* --- Base32 decode (RFC 4648) --- */ function base32_decode(input) { var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; input = input.replace(/[\s=]/g, '').toUpperCase(); var bits = 0, value = 0, idx = 0; var output = new Uint8Array(Math.floor(input.length * 5 / 8)); for (var i = 0; i < input.length; i++) { var v = alphabet.indexOf(input[i]); if (v < 0) continue; value = (value << 5) | v; bits += 5; if (bits >= 8) { output[idx++] = (value >>> (bits - 8)) & 0xFF; bits -= 8; } } return new Uint8Array(output.buffer, 0, idx); } /* --- HMAC-SHA1 --- */ function hmac_sha1(key, data) { if (IS_BROWSER_TOTP) { return crypto.subtle.importKey( 'raw', key, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign'] ).then(function(cryptoKey) { return crypto.subtle.sign('HMAC', cryptoKey, data); }).then(function(sig) { return new Uint8Array(sig); }); } else { return native_hmac_sha1(key, data); } } /* --- TOTP (RFC 6238) --- */ /** * Generate a TOTP code. * @param {string} secret_b32 - base32-encoded TOTP secret * @param {number} [time] - Unix timestamp (default: now) * @param {number} [period] - Time step in seconds (default: 30) * @param {number} [digits] - Number of digits (default: 6) * @returns {string|Promise} TOTP code (zero-padded) */ function generate_totp(secret_b32, time, period, digits) { period = period || 30; digits = digits || 6; time = time || Math.floor(Date.now() / 1000); var key = base32_decode(secret_b32); var counter = Math.floor(time / period); /* Encode counter as 8-byte big-endian */ var msg = new Uint8Array(8); var tmp = counter; for (var i = 7; i >= 0; i--) { msg[i] = tmp & 0xFF; tmp = Math.floor(tmp / 256); } function truncate(hash) { var offset = hash[hash.length - 1] & 0x0F; var code = ( ((hash[offset] & 0x7F) << 24) | ((hash[offset + 1] & 0xFF) << 16) | ((hash[offset + 2] & 0xFF) << 8) | (hash[offset + 3] & 0xFF) ) % Math.pow(10, digits); var s = code.toString(); while (s.length < digits) s = '0' + s; return s; } if (IS_BROWSER_TOTP) { return hmac_sha1(key, msg).then(truncate); } else { return truncate(hmac_sha1(key, msg)); } } /** * Time remaining until current TOTP code expires. * @param {number} [period] - Time step (default: 30) * @returns {number} seconds remaining */ function totp_remaining(period) { period = period || 30; return period - (Math.floor(Date.now() / 1000) % period); } /* Export */ if (typeof globalThis.vault1984 === 'undefined') globalThis.vault1984 = {}; globalThis.vault1984.totp = { generate_totp: generate_totp, totp_remaining: totp_remaining, base32_decode: base32_decode };