bellstrand/totp-generator

View on GitHub
src/index.ts

Summary

Maintainability
A
0 mins
Test Coverage
import JsSHA from "jssha"

export type TOTPAlgorithm = "SHA-1" | "SHA-224" | "SHA-256" | "SHA-384" | "SHA-512" | "SHA3-224" | "SHA3-256" | "SHA3-384" | "SHA3-512"

/**
 * @param {string} [digits=6]
 * @param {string} [algorithm="SHA-1"]
 * @param {string} [period=30]
 * @param {string} [timestamp=Date.now()]
 */
type Options = {
    digits?: number
    algorithm?: TOTPAlgorithm
    period?: number
    timestamp?: number
}

export class TOTP {
    static generate(key: string, options?: Options) {
        const _options: Required<Options> = { digits: 6, algorithm: "SHA-1", period: 30, timestamp: Date.now(), ...options }
        const epoch = Math.floor(_options.timestamp / 1000.0)
        const time = this.leftpad(this.dec2hex(Math.floor(epoch / _options.period)), 16, "0")
        const shaObj = new JsSHA(_options.algorithm, "HEX")
        shaObj.setHMACKey(this.base32tohex(key), "HEX")
        shaObj.update(time)
        const hmac = shaObj.getHMAC("HEX")
        const offset = this.hex2dec(hmac.substring(hmac.length - 1))
        let otp = (this.hex2dec(hmac.substr(offset * 2, 8)) & this.hex2dec("7fffffff")) + ""
        const start = Math.max(otp.length - _options.digits, 0)
        otp = otp.substring(start, start + _options.digits)
        const expires = Math.ceil((_options.timestamp + 1) / (_options.period * 1000)) * _options.period * 1000
        return { otp, expires }
    }

    private static hex2dec(hex: string) {
        return parseInt(hex, 16)
    }

    private static dec2hex(dec: number) {
        return (dec < 15.5 ? "0" : "") + Math.round(dec).toString(16)
    }

    private static base32tohex(base32: string) {
        const base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
        let bits = ""
        let hex = ""

        const _base32 = base32.replace(/=+$/, "")

        for (let i = 0; i < _base32.length; i++) {
            const val = base32chars.indexOf(base32.charAt(i).toUpperCase())
            if (val === -1) throw new Error("Invalid base32 character in key")
            bits += this.leftpad(val.toString(2), 5, "0")
        }

        for (let i = 0; i + 8 <= bits.length; i += 8) {
            const chunk = bits.substr(i, 8)
            hex = hex + this.leftpad(parseInt(chunk, 2).toString(16), 2, "0")
        }
        return hex
    }

    private static leftpad(str: string, len: number, pad: string) {
        if (len + 1 >= str.length) {
            str = Array(len + 1 - str.length).join(pad) + str
        }
        return str
    }
}