116 lines
3.4 KiB
JavaScript
116 lines
3.4 KiB
JavaScript
/*
|
|
* clavitor — 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.clavitor === 'undefined') globalThis.clavitor = {};
|
|
globalThis.clavitor.totp = {
|
|
generate_totp: generate_totp,
|
|
totp_remaining: totp_remaining,
|
|
base32_decode: base32_decode
|
|
};
|