vault1984/oss/crypto/totp.js

116 lines
3.4 KiB
JavaScript

/*
* 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<string>} 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
};