feat: changed TOTP and base32 library to self-writed special for quick-js, fixed BigInt incompatibility

This commit is contained in:
Савелий Савенок
2024-11-13 02:30:53 +03:00
parent ab13957cb0
commit f16d7374f4
14 changed files with 147 additions and 328 deletions

View File

@@ -0,0 +1,54 @@
import { decode } from "./base32decoder.js";
import jsSHA from "jssha";
"use bigint"
/**
* get HOTP based on counter
* @param {BigInt} counter BigInt counter of HOTP
* @param {string} secret base32 encoded string
* @param {number} [digits=6] number of digits in OTP token
* @param {string} [hashType='SHA-1'] type of hash (more in jsSHA documentation)
* @returns HOTP string
*/
export function getHOTP(counter, secret, digits = 6, hashType = 'SHA-1'){
//Stage 1: Prepare data
const rawDataCounter = new DataView(new ArrayBuffer(8))
rawDataCounter.setUint32(0, counter >> 32)
rawDataCounter.setUint32(4, counter)
const bCounter = new Uint8Array(rawDataCounter.buffer)
const bSecret = new Uint8Array(decode(secret).match(/.{1,2}/g).map(chunk => parseInt(chunk, 16))); //confirmed
//Stage 2: Hash data
const jssha = new jsSHA(hashType, 'UINT8ARRAY')
jssha.setHMACKey(bSecret, 'UINT8ARRAY')
jssha.update(bCounter)
const hmacResult = jssha.getHMAC('UINT8ARRAY') //confirmed
//Stage 3: Dynamic truncate
const offsetB = hmacResult[19] & 0xf;
const P = hmacResult.slice(offsetB, offsetB + 4)
P[0] = P[0] & 0x7f;
//Stage 4: Format string
let res = (new DataView(P.buffer).getInt32(0) % Math.pow(10, digits)).toString()
while(res.length < digits)
res = '0' + res;
return res;
}
/**
* get OTP based on current time
* @param {string} secret base32 encoded string
* @param {number} [digits=6] digits in OTP
* @param {number} [time=Date.now()] time for counter (default unix time epoch)
* @param {number} [fetchTime=30] period of token in seconds
* @param {number} [timeOffset=0] time offset for token in seconds
* @param {string} [hashType='SHA-1'] type of hash (more in jsSHA documentation)
* @returns TOTP string
*/
export function getTOTP(secret, digits = 6, time = Date.now(), fetchTime = 30, timeOffset = 0, hashType = 'SHA-1')
{
const unixTime = Math.round((time / 1000 + timeOffset) / fetchTime)
return getHOTP(BigInt(unixTime), secret, digits)
}

View File

@@ -0,0 +1,26 @@
export function decode(base32) {
for (
var base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",
bits = "",
hex = "",
i = 0;
i < base32.length;
i++
) {
var val = base32chars.indexOf(base32.charAt(i).toUpperCase());
bits += leftpad(val.toString(2), 5, "0");
}
for (i = 0; i + 4 <= bits.length; i += 4) {
var chunk = bits.substr(i, 4);
hex += parseInt(chunk, 2).toString(16);
}
return hex;
}
function leftpad(str, len, pad) {
return (
len + 1 >= str.length &&
(str = new Array(len + 1 - str.length).join(pad) + str),
str
);
}

61
lib/totp-quickjs/index.js Normal file
View File

@@ -0,0 +1,61 @@
import { getHOTP } from "./OTPGenerator.js"
"use bigint"
/**
* TOTP instance
*/
export class TOTP{
/**
*
* @param {string} secret base32 encoded string
* @param {string} issuer issuer of TOTP
* @param {number} [digits=6] number of digits in OTP token
* @param {number} [fetchTime=30] period of token in seconds
* @param {number} [timeOffset=0] time offset for token in seconds
* @param {string} [hashType='SHA-1'] type of hash (more in jsSHA documentation)
*/
constructor(secret,
issuer,
digits = 6,
fetchTime = 30,
timeOffset = 0,
hashType = 'SHA-1')
{
this.secret = secret
this.issuer = issuer
this.digits = digits
this.fetchTime = fetchTime
this.timeOffset = timeOffset
this.hashType = hashType
}
/**
*
* @param {number} time time for counter (default unix time epoch)
* @returns OTP instance
*/
getOTP(time = Date.now()){
const unixTime = (time / 1000 + this.timeOffset) / this.fetchTime
const otp = getHOTP(Math.floor(unixTime), this.secret, this.digits)
const expireTime = time +
(this.fetchTime -
(time / 1000 + this.timeOffset) %
this.fetchTime) * 1000
return new OTP(otp, expireTime)
}
}
/**
* Class for TOTP.getOTP result
*/
export class OTP{
/**
*
* @param {string} otp OTP string
* @param {number} expireTime time in seconds to reset OTP
*/
constructor(otp, expireTime)
{
this.otp = otp
this.expireTime = expireTime
}
}