All files / lib/esm/Service OTPService.js

0% Statements 0/47
0% Branches 0/27
0% Functions 0/7
0% Lines 0/47

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127                                                                                                                                                                                                                                                             
import * as OTPAuth from "otpauth";
import qrcode from "qrcode-generator";
import qrcodeParser from "qrcode-parser";
import { OTPAlgorithms } from "../Interfaces/Credential/OTPConfigInterface";
import { assertDocumentAvailable } from "../Util/DomEnvironment";
/**
 * OTP helpers. Token generation ({@link OTPService.updateOTP}) and otpauth URI building
 * ({@link OTPService.getDataUrlFromCurrentOTPValues}) are SW-safe (no DOM).
 * QR image rendering ({@link OTPService.updateQRFromCurrentOTPValues}) and QR file parsing
 * ({@link OTPService.parseOTPQrCodeFromInputFileData}) require a DOM and are guarded for MV3.
 */
export class OTPService {
    static mergeDefaultOTPConfig = (otp) => {
        const defaults = {
            algorithm: "SHA1",
            period: 30,
            digits: 6,
        };
        Object.assign(otp, { ...defaults, ...otp });
    };
    static getSecretString = (secret) => {
        if (typeof secret == "object") {
            return secret.utf8;
        }
        return secret;
    };
    static getDataUrlFromCurrentOTPValues = (otp) => {
        const totp = new OTPAuth.TOTP({
            issuer: otp.issuer,
            label: otp.label,
            algorithm: otp.algorithm,
            digits: otp.digits,
            period: otp.period,
            secret: otp.secret
        });
        return decodeURIComponent(totp.toString());
    };
    /**
     * Updates otp.qr_uri with a new QR code image and otpauth data URL based on the raw otp values.
     * Requires a DOM (canvas). Not available in MV3 service workers. Generate the token/URI there
     * with {@link OTPService.updateOTP} / {@link OTPService.getDataUrlFromCurrentOTPValues}, and
     * render the QR in a page/popup/offscreen document.
     * @param otp
     * @throws Error when called without a DOM
     */
    static updateQRFromCurrentOTPValues = (otp) => {
        assertDocumentAvailable("OTPService.updateQRFromCurrentOTPValues");
        if (otp.qr_uri === undefined) {
            otp.qr_uri = {};
        }
        const typeNumberAutoDetect = 0;
        const errorCorrectionLevel = 'L'; // L = 7%
        const qr = qrcode(typeNumberAutoDetect, errorCorrectionLevel);
        if (typeof otp.qr_uri == "object") {
            otp.qr_uri.qrData = OTPService.getDataUrlFromCurrentOTPValues(otp);
        }
        else if (typeof otp.qr_uri == "string") {
            otp.qr_uri = {
                qrData: otp.qr_uri,
                image: null
            };
        }
        qr.addData(otp.qr_uri.qrData);
        qr.make();
        const cellSize = 4;
        const padding = 0;
        const canvas = document.createElement("canvas");
        canvas.width = canvas.height = qr.getModuleCount() * cellSize + padding * 2;
        const context = canvas.getContext("2d");
        qr.renderTo2dContext(context, cellSize);
        otp.qr_uri.image = canvas.toDataURL("image/png");
        canvas.remove();
    };
    /**
     * Returns the current token of the given OTP configuration.
     * @param otp
     */
    static updateOTP = (otp) => {
        if (!otp || !otp.secret || otp.secret === "") {
            return;
        }
        if (typeof otp.secret == "string" && otp.secret.includes(' ')) {
            otp.secret = otp.secret.replaceAll(' ', '');
        }
        OTPService.mergeDefaultOTPConfig(otp);
        const totp = new OTPAuth.TOTP({
            issuer: otp.issuer,
            label: otp.label,
            algorithm: otp.algorithm,
            digits: otp.digits,
            period: otp.period,
            secret: otp.secret
        });
        return totp.generate();
    };
    /**
     * Parse input file asynchronous as QR code, extract the TOTP values and return the IOTPConfig, that's built from it.
     * Requires a DOM (qrcode-parser uses browser image APIs). Not available in MV3 service workers.
     * @param input
     * @throws Error when called without a DOM
     */
    static parseOTPQrCodeFromInputFileData = async (input) => {
        assertDocumentAvailable("OTPService.parseOTPQrCodeFromInputFileData");
        return await qrcodeParser(input).then((otpUrl) => {
            const uri = new URL(otpUrl);
            const type = (uri.href.indexOf('totp/') !== -1) ? 'totp' : 'hotp';
            const label = uri.pathname.replace('//' + type + '/', '');
            const otp = {
                type: type,
                label: decodeURIComponent(label),
                qr_uri: {
                    qrData: decodeURIComponent(otpUrl),
                    image: input
                },
                issuer: uri.searchParams.get('issuer'),
                secret: uri.searchParams.get('secret'),
                algorithm: uri.searchParams.get('algorithm') && OTPAlgorithms.includes(uri.searchParams.get('algorithm'))
                    ? uri.searchParams.get('algorithm')
                    : "SHA1",
                period: uri.searchParams.get('period') ? parseInt(uri.searchParams.get('period')) : 30,
                digits: uri.searchParams.get('digits') ? parseInt(uri.searchParams.get('digits')) : 6,
            };
            return otp;
        });
    };
}