module.allowclient = true;

// https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/
// https://lapo.it/asn1js/#MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgdtVjL6oGqHBkD_SAu21f9qTJRi5s1Se0KGgWwj-K-kyhRANCAARtS6Pi8viOgF3GvXA8XZop4jj9LHDW5NS-Z6YHPc5I-a93SXyD6MVj-Gf4rQh41P5fZIXVU957tJIh0VU5hv7G
// https://datatracker.ietf.org/doc/html/rfc8410#section-10.3

// NOTE: We could make this synchronous by moving sha512 to be inline (it is about 1000 lines),
//  and by removing support for RSA (which is fine, there is no reason to move back to it from ED25519).
export async function signWithPEM(config: {
    pem: Buffer | string;
    data: object | string | number | boolean;
}): Promise<Buffer> {
    let { pem, data } = config;
    let inputMessageStr = JSON.stringify(data);
    let inputMessage = Buffer.from(inputMessageStr);
    function isED25519(key: string | Buffer) {
        return key.length < 256;
    }
    if (isED25519(pem)) {
        let key = extractRawED25519PrivateKey(pem);
        let signature = await signED25519(key, inputMessage);
        return signature;
    } else {
        let key = await globalThis.crypto.subtle.importKey("spki", Buffer.from(pem as any), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["sign"]);
        let signatureBuffer = await globalThis.crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, inputMessage);
        return Buffer.from(signatureBuffer);
    }
}

export function extractRawED25519PrivateKey(pem: string | Buffer) {
    let keyPart = pem.toString().replaceAll("-----BEGIN PRIVATE KEY-----", "").replaceAll("-----END PRIVATE KEY-----", "").replaceAll("\n", "").replaceAll("\r", "");
    let forgeParsed = berTagParser(Buffer.from(keyPart, "base64"));
    return forgeParsed[0].children![2].data.slice(2, 2 + 32);
}
function wrapWithPEM(rawED25519Key: Buffer) {
    let forgeBase = encodeBerTags([
        { type: tagLookup.OCTET_STRING, data: rawED25519Key, }
    ]);
    let forgeValue = encodeBerTags([
        {
            type: tagLookup.SEQUENCE,
            children: [
                { type: tagLookup.INTEGER, data: [0], },
                {
                    type: tagLookup.SEQUENCE,
                    children: [
                        { type: tagLookup.OBJECT_IDENTIFIER, data: [43, 101, 112], }
                    ]
                },
                { type: tagLookup.OCTET_STRING, data: forgeBase, }
            ],
        }
    ]);

    return "-----BEGIN PRIVATE KEY-----\n" + forgeValue.toString("base64") + "\n-----END PRIVATE KEY-----";
}

export async function signED25519(privateKey: Buffer, inputMessage: Buffer): Promise<Buffer> {
    if (privateKey.length !== 32) {
        throw new Error(`Invalid private key length, expected 32, got ${privateKey.length}`);
    }
    var pk = Buffer.alloc(32);
    var sk = Buffer.alloc(64);
    privateKey.copy(sk);
    await crypto_sign_keypair(pk, sk);

    let signedOutput = Buffer.alloc(64 + inputMessage.length);
    await crypto_sign(signedOutput, inputMessage, sk);
    return signedOutput.slice(0, 64);
}
export async function extractPublicKey(privateKey: Buffer): Promise<Buffer> {
    if (privateKey.length !== 32) throw new Error(`Invalid private key length, expected 32, got ${privateKey.length}. Only ED25519 is presently supported`);
    var pk = Buffer.alloc(32);
    var sk = Buffer.alloc(64);
    privateKey.copy(sk);
    await crypto_sign_keypair(pk, sk);
    return pk;
}
export async function verifyED25519(publicKey: Buffer, inputMessage: Buffer, signature: Buffer): Promise<boolean> {
    var sm = Buffer.alloc(64 + inputMessage.length);
    var m = Buffer.alloc(64 + inputMessage.length);
    var i;
    for (i = 0; i < 64; ++i) {
        sm[i] = signature[i];
    }
    for (i = 0; i < inputMessage.length; i++) {
        sm[i + 64] = inputMessage[i];
    }
    let resultLen = await crypto_sign_open(m, sm, sm.length, publicKey);
    return resultLen >= 0;
}

var gf0 = gf();
var gf1 = gf([1]);
var D = gf([
    0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070,
    0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203]);
var D2 = gf([
    0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0,
    0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406]);
var X = gf([
    0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c,
    0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169]);
var Y = gf([
    0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666,
    0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666]);
var L = new Float64Array([
    0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58,
    0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10]);
var I = gf([
    0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43,
    0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83]);

// TODO: update forge buffer implementation to use `Buffer` or `Uint8Array`,
// whichever is available, to improve performance
async function sha512(msg: Uint8Array, msgLen: number): Promise<Buffer> {
    let result = await globalThis.crypto.subtle.digest("SHA-512", msg.slice(0, msgLen));
    return Buffer.from(result);
    //return Buffer.from(sha512js.sha512(msg.slice(0, msgLen)), "hex");
}

async function crypto_sign_keypair(publicKey: Buffer, secretKey: Buffer) {
    var p = [gf(), gf(), gf(), gf()];
    var i;

    var d = await sha512(secretKey, 32);
    d[0] &= 248;
    d[31] &= 127;
    d[31] |= 64;

    scalarbase(p, d);
    pack(publicKey, p);

    for (i = 0; i < 32; ++i) {
        secretKey[i + 32] = publicKey[i];
    }
}

// Note: difference from C - smlen returned, not passed as argument.
async function crypto_sign(sm: Buffer, m: Buffer, sk: Buffer) {
    let n = m.length;
    var i, j, x = new Float64Array(64);
    var p = [gf(), gf(), gf(), gf()];

    var d = await sha512(sk, 32);
    d[0] &= 248;
    d[31] &= 127;
    d[31] |= 64;

    var smlen = n + 64;
    for (i = 0; i < n; ++i) {
        sm[64 + i] = m[i];
    }
    for (i = 0; i < 32; ++i) {
        sm[32 + i] = d[32 + i];
    }

    var r = await sha512(sm.subarray(32), n + 32);
    reduce(r);
    scalarbase(p, r);
    pack(sm, p);

    for (i = 32; i < 64; ++i) {
        sm[i] = sk[i];
    }
    var h = await sha512(sm, n + 64);
    reduce(h);

    for (i = 32; i < 64; ++i) {
        x[i] = 0;
    }
    for (i = 0; i < 32; ++i) {
        x[i] = r[i];
    }
    for (i = 0; i < 32; ++i) {
        for (j = 0; j < 32; j++) {
            x[i + j] += h[i] * d[j];
        }
    }

    modL(sm.subarray(32), x);
    return smlen;
}

async function crypto_sign_open(m: Buffer, sm: Buffer, n: number, pk: Buffer) {
    var i, mlen;
    var t = Buffer.alloc(32);
    var p = [gf(), gf(), gf(), gf()],
        q = [gf(), gf(), gf(), gf()];

    mlen = -1;
    if (n < 64) {
        return -1;
    }

    if (unpackneg(q, pk)) {
        return -1;
    }

    for (i = 0; i < n; ++i) {
        m[i] = sm[i];
    }
    for (i = 0; i < 32; ++i) {
        m[i + 32] = pk[i];
    }
    var h = await sha512(m, n);
    reduce(h);
    scalarmult(p, q, h);

    scalarbase(q, sm.subarray(32));
    add(p, q);
    pack(t, p);

    n -= 64;
    if (crypto_verify_32(sm, 0, t, 0)) {
        for (i = 0; i < n; ++i) {
            m[i] = 0;
        }
        return -1;
    }

    for (i = 0; i < n; ++i) {
        m[i] = sm[i + 64];
    }
    mlen = n;
    return mlen;
}

function modL(r: Buffer, x: Buffer | Float64Array) {
    var carry, i, j, k;
    for (i = 63; i >= 32; --i) {
        carry = 0;
        for (j = i - 32, k = i - 12; j < k; ++j) {
            x[j] += carry - 16 * x[i] * L[j - (i - 32)];
            carry = (x[j] + 128) >> 8;
            x[j] -= carry * 256;
        }
        x[j] += carry;
        x[i] = 0;
    }
    carry = 0;
    for (j = 0; j < 32; ++j) {
        x[j] += carry - (x[31] >> 4) * L[j];
        carry = x[j] >> 8;
        x[j] &= 255;
    }
    for (j = 0; j < 32; ++j) {
        x[j] -= carry * L[j];
    }
    for (i = 0; i < 32; ++i) {
        x[i + 1] += x[i] >> 8;
        r[i] = x[i] & 255;
    }
}

function reduce(r: Buffer) {
    var x = new Float64Array(64);
    for (var i = 0; i < 64; ++i) {
        x[i] = r[i];
        r[i] = 0;
    }
    modL(r, x);
}

function add(p: Buffer[] | Float64Array[], q: Buffer[] | Float64Array[]) {
    var a = gf(), b = gf(), c = gf(),
        d = gf(), e = gf(), f = gf(),
        g = gf(), h = gf(), t = gf();

    Z(a, p[1], p[0]);
    Z(t, q[1], q[0]);
    M(a, a, t);
    A(b, p[0], p[1]);
    A(t, q[0], q[1]);
    M(b, b, t);
    M(c, p[3], q[3]);
    M(c, c, D2);
    M(d, p[2], q[2]);
    A(d, d, d);
    Z(e, b, a);
    Z(f, d, c);
    A(g, d, c);
    A(h, b, a);

    M(p[0], e, f);
    M(p[1], h, g);
    M(p[2], g, f);
    M(p[3], e, h);
}

function cswap(p: Buffer[] | Float64Array[], q: Buffer[] | Float64Array[], b: number) {
    for (var i = 0; i < 4; ++i) {
        sel25519(p[i], q[i], b);
    }
}

function pack(r: Buffer, p: Buffer[] | Float64Array[]) {
    var tx = gf(), ty = gf(), zi = gf();
    inv25519(zi, p[2]);
    M(tx, p[0], zi);
    M(ty, p[1], zi);
    pack25519(r, ty);
    r[31] ^= par25519(tx) << 7;
}

function pack25519(o: Buffer, n: Buffer | Float64Array) {
    var i, j, b;
    var m = gf(), t = gf();
    for (i = 0; i < 16; ++i) {
        t[i] = n[i];
    }
    car25519(t);
    car25519(t);
    car25519(t);
    for (j = 0; j < 2; ++j) {
        m[0] = t[0] - 0xffed;
        for (i = 1; i < 15; ++i) {
            m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
            m[i - 1] &= 0xffff;
        }
        m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
        b = (m[15] >> 16) & 1;
        m[14] &= 0xffff;
        sel25519(t, m, 1 - b);
    }
    for (i = 0; i < 16; i++) {
        o[2 * i] = t[i] & 0xff;
        o[2 * i + 1] = t[i] >> 8;
    }
}

function unpackneg(r: Float64Array[], p: Buffer) {
    var t = gf(), chk = gf(), num = gf(),
        den = gf(), den2 = gf(), den4 = gf(),
        den6 = gf();

    set25519(r[2], gf1);
    unpack25519(r[1], p);
    S(num, r[1]);
    M(den, num, D);
    Z(num, num, r[2]);
    A(den, r[2], den);

    S(den2, den);
    S(den4, den2);
    M(den6, den4, den2);
    M(t, den6, num);
    M(t, t, den);

    pow2523(t, t);
    M(t, t, num);
    M(t, t, den);
    M(t, t, den);
    M(r[0], t, den);

    S(chk, r[0]);
    M(chk, chk, den);
    if (neq25519(chk, num)) {
        M(r[0], r[0], I);
    }

    S(chk, r[0]);
    M(chk, chk, den);
    if (neq25519(chk, num)) {
        return -1;
    }

    if (par25519(r[0]) === (p[31] >> 7)) {
        Z(r[0], gf0, r[0]);
    }

    M(r[3], r[0], r[1]);
    return 0;
}

function unpack25519(o: Float64Array, n: Buffer) {
    var i;
    for (i = 0; i < 16; ++i) {
        o[i] = n[2 * i] + (n[2 * i + 1] << 8);
    }
    o[15] &= 0x7fff;
}

function pow2523(o: Float64Array, i: Float64Array) {
    var c = gf();
    var a;
    for (a = 0; a < 16; ++a) {
        c[a] = i[a];
    }
    for (a = 250; a >= 0; --a) {
        S(c, c);
        if (a !== 1) {
            M(c, c, i);
        }
    }
    for (a = 0; a < 16; ++a) {
        o[a] = c[a];
    }
}

function neq25519(a: Float64Array, b: Float64Array) {
    var c = Buffer.alloc(32);
    var d = Buffer.alloc(32);
    pack25519(c, a);
    pack25519(d, b);
    return crypto_verify_32(c, 0, d, 0);
}

function crypto_verify_32(x: Buffer, xi: number, y: Buffer, yi: number) {
    return vn(x, xi, y, yi, 32);
}

function vn(x: Buffer, xi: number, y: Buffer, yi: number, n: number) {
    var i, d = 0;
    for (i = 0; i < n; ++i) {
        d |= x[xi + i] ^ y[yi + i];
    }
    return (1 & ((d - 1) >>> 8)) - 1;
}

function par25519(a: Float64Array) {
    var d = Buffer.alloc(32);
    pack25519(d, a);
    return d[0] & 1;
}

function scalarmult(p: Buffer[] | Float64Array[], q: Buffer[] | Float64Array[], s: Buffer) {
    var b, i;
    set25519(p[0], gf0);
    set25519(p[1], gf1);
    set25519(p[2], gf1);
    set25519(p[3], gf0);
    for (i = 255; i >= 0; --i) {
        b = (s[(i / 8) | 0] >> (i & 7)) & 1;
        cswap(p, q, b);
        add(q, p);
        add(p, p);
        cswap(p, q, b);
    }
}

function scalarbase(p: Buffer[] | Float64Array[], s: Buffer) {
    var q = [gf(), gf(), gf(), gf()];
    set25519(q[0], X);
    set25519(q[1], Y);
    set25519(q[2], gf1);
    M(q[3], X, Y);
    scalarmult(p, q, s);
}

function set25519(r: Float64Array | Buffer, a: Float64Array | Buffer) {
    var i;
    for (i = 0; i < 16; i++) {
        r[i] = a[i] | 0;
    }
}

function inv25519(o: Float64Array, i: Float64Array | Buffer) {
    var c = gf();
    var a;
    for (a = 0; a < 16; ++a) {
        c[a] = i[a];
    }
    for (a = 253; a >= 0; --a) {
        S(c, c);
        if (a !== 2 && a !== 4) {
            M(c, c, i);
        }
    }
    for (a = 0; a < 16; ++a) {
        o[a] = c[a];
    }
}

function car25519(o: Float64Array) {
    var i, v, c = 1;
    for (i = 0; i < 16; ++i) {
        v = o[i] + c + 65535;
        c = Math.floor(v / 65536);
        o[i] = v - c * 65536;
    }
    o[0] += c - 1 + 37 * (c - 1);
}

function sel25519(p: Buffer | Float64Array, q: Buffer | Float64Array, b: number) {
    var t, c = ~(b - 1);
    for (var i = 0; i < 16; ++i) {
        t = c & (p[i] ^ q[i]);
        p[i] ^= t;
        q[i] ^= t;
    }
}

function gf(init?: Buffer | number[]) {
    var i, r = new Float64Array(16);
    if (init) {
        for (i = 0; i < init.length; ++i) {
            r[i] = init[i];
        }
    }
    return r;
}

function A(o: Float64Array | Buffer, a: Float64Array | Buffer, b: Float64Array | Buffer) {
    for (var i = 0; i < 16; ++i) {
        o[i] = a[i] + b[i];
    }
}

function Z(o: Float64Array | Buffer, a: Float64Array | Buffer, b: Float64Array | Buffer) {
    for (var i = 0; i < 16; ++i) {
        o[i] = a[i] - b[i];
    }
}

function S(o: Float64Array, a: Float64Array) {
    M(o, a, a);
}

function M(o: Float64Array | Buffer, a: Float64Array | Buffer, b: Float64Array | Buffer) {
    var v, c,
        t0 = 0, t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0, t6 = 0, t7 = 0,
        t8 = 0, t9 = 0, t10 = 0, t11 = 0, t12 = 0, t13 = 0, t14 = 0, t15 = 0,
        t16 = 0, t17 = 0, t18 = 0, t19 = 0, t20 = 0, t21 = 0, t22 = 0, t23 = 0,
        t24 = 0, t25 = 0, t26 = 0, t27 = 0, t28 = 0, t29 = 0, t30 = 0,
        b0 = b[0],
        b1 = b[1],
        b2 = b[2],
        b3 = b[3],
        b4 = b[4],
        b5 = b[5],
        b6 = b[6],
        b7 = b[7],
        b8 = b[8],
        b9 = b[9],
        b10 = b[10],
        b11 = b[11],
        b12 = b[12],
        b13 = b[13],
        b14 = b[14],
        b15 = b[15];

    v = a[0];
    t0 += v * b0;
    t1 += v * b1;
    t2 += v * b2;
    t3 += v * b3;
    t4 += v * b4;
    t5 += v * b5;
    t6 += v * b6;
    t7 += v * b7;
    t8 += v * b8;
    t9 += v * b9;
    t10 += v * b10;
    t11 += v * b11;
    t12 += v * b12;
    t13 += v * b13;
    t14 += v * b14;
    t15 += v * b15;
    v = a[1];
    t1 += v * b0;
    t2 += v * b1;
    t3 += v * b2;
    t4 += v * b3;
    t5 += v * b4;
    t6 += v * b5;
    t7 += v * b6;
    t8 += v * b7;
    t9 += v * b8;
    t10 += v * b9;
    t11 += v * b10;
    t12 += v * b11;
    t13 += v * b12;
    t14 += v * b13;
    t15 += v * b14;
    t16 += v * b15;
    v = a[2];
    t2 += v * b0;
    t3 += v * b1;
    t4 += v * b2;
    t5 += v * b3;
    t6 += v * b4;
    t7 += v * b5;
    t8 += v * b6;
    t9 += v * b7;
    t10 += v * b8;
    t11 += v * b9;
    t12 += v * b10;
    t13 += v * b11;
    t14 += v * b12;
    t15 += v * b13;
    t16 += v * b14;
    t17 += v * b15;
    v = a[3];
    t3 += v * b0;
    t4 += v * b1;
    t5 += v * b2;
    t6 += v * b3;
    t7 += v * b4;
    t8 += v * b5;
    t9 += v * b6;
    t10 += v * b7;
    t11 += v * b8;
    t12 += v * b9;
    t13 += v * b10;
    t14 += v * b11;
    t15 += v * b12;
    t16 += v * b13;
    t17 += v * b14;
    t18 += v * b15;
    v = a[4];
    t4 += v * b0;
    t5 += v * b1;
    t6 += v * b2;
    t7 += v * b3;
    t8 += v * b4;
    t9 += v * b5;
    t10 += v * b6;
    t11 += v * b7;
    t12 += v * b8;
    t13 += v * b9;
    t14 += v * b10;
    t15 += v * b11;
    t16 += v * b12;
    t17 += v * b13;
    t18 += v * b14;
    t19 += v * b15;
    v = a[5];
    t5 += v * b0;
    t6 += v * b1;
    t7 += v * b2;
    t8 += v * b3;
    t9 += v * b4;
    t10 += v * b5;
    t11 += v * b6;
    t12 += v * b7;
    t13 += v * b8;
    t14 += v * b9;
    t15 += v * b10;
    t16 += v * b11;
    t17 += v * b12;
    t18 += v * b13;
    t19 += v * b14;
    t20 += v * b15;
    v = a[6];
    t6 += v * b0;
    t7 += v * b1;
    t8 += v * b2;
    t9 += v * b3;
    t10 += v * b4;
    t11 += v * b5;
    t12 += v * b6;
    t13 += v * b7;
    t14 += v * b8;
    t15 += v * b9;
    t16 += v * b10;
    t17 += v * b11;
    t18 += v * b12;
    t19 += v * b13;
    t20 += v * b14;
    t21 += v * b15;
    v = a[7];
    t7 += v * b0;
    t8 += v * b1;
    t9 += v * b2;
    t10 += v * b3;
    t11 += v * b4;
    t12 += v * b5;
    t13 += v * b6;
    t14 += v * b7;
    t15 += v * b8;
    t16 += v * b9;
    t17 += v * b10;
    t18 += v * b11;
    t19 += v * b12;
    t20 += v * b13;
    t21 += v * b14;
    t22 += v * b15;
    v = a[8];
    t8 += v * b0;
    t9 += v * b1;
    t10 += v * b2;
    t11 += v * b3;
    t12 += v * b4;
    t13 += v * b5;
    t14 += v * b6;
    t15 += v * b7;
    t16 += v * b8;
    t17 += v * b9;
    t18 += v * b10;
    t19 += v * b11;
    t20 += v * b12;
    t21 += v * b13;
    t22 += v * b14;
    t23 += v * b15;
    v = a[9];
    t9 += v * b0;
    t10 += v * b1;
    t11 += v * b2;
    t12 += v * b3;
    t13 += v * b4;
    t14 += v * b5;
    t15 += v * b6;
    t16 += v * b7;
    t17 += v * b8;
    t18 += v * b9;
    t19 += v * b10;
    t20 += v * b11;
    t21 += v * b12;
    t22 += v * b13;
    t23 += v * b14;
    t24 += v * b15;
    v = a[10];
    t10 += v * b0;
    t11 += v * b1;
    t12 += v * b2;
    t13 += v * b3;
    t14 += v * b4;
    t15 += v * b5;
    t16 += v * b6;
    t17 += v * b7;
    t18 += v * b8;
    t19 += v * b9;
    t20 += v * b10;
    t21 += v * b11;
    t22 += v * b12;
    t23 += v * b13;
    t24 += v * b14;
    t25 += v * b15;
    v = a[11];
    t11 += v * b0;
    t12 += v * b1;
    t13 += v * b2;
    t14 += v * b3;
    t15 += v * b4;
    t16 += v * b5;
    t17 += v * b6;
    t18 += v * b7;
    t19 += v * b8;
    t20 += v * b9;
    t21 += v * b10;
    t22 += v * b11;
    t23 += v * b12;
    t24 += v * b13;
    t25 += v * b14;
    t26 += v * b15;
    v = a[12];
    t12 += v * b0;
    t13 += v * b1;
    t14 += v * b2;
    t15 += v * b3;
    t16 += v * b4;
    t17 += v * b5;
    t18 += v * b6;
    t19 += v * b7;
    t20 += v * b8;
    t21 += v * b9;
    t22 += v * b10;
    t23 += v * b11;
    t24 += v * b12;
    t25 += v * b13;
    t26 += v * b14;
    t27 += v * b15;
    v = a[13];
    t13 += v * b0;
    t14 += v * b1;
    t15 += v * b2;
    t16 += v * b3;
    t17 += v * b4;
    t18 += v * b5;
    t19 += v * b6;
    t20 += v * b7;
    t21 += v * b8;
    t22 += v * b9;
    t23 += v * b10;
    t24 += v * b11;
    t25 += v * b12;
    t26 += v * b13;
    t27 += v * b14;
    t28 += v * b15;
    v = a[14];
    t14 += v * b0;
    t15 += v * b1;
    t16 += v * b2;
    t17 += v * b3;
    t18 += v * b4;
    t19 += v * b5;
    t20 += v * b6;
    t21 += v * b7;
    t22 += v * b8;
    t23 += v * b9;
    t24 += v * b10;
    t25 += v * b11;
    t26 += v * b12;
    t27 += v * b13;
    t28 += v * b14;
    t29 += v * b15;
    v = a[15];
    t15 += v * b0;
    t16 += v * b1;
    t17 += v * b2;
    t18 += v * b3;
    t19 += v * b4;
    t20 += v * b5;
    t21 += v * b6;
    t22 += v * b7;
    t23 += v * b8;
    t24 += v * b9;
    t25 += v * b10;
    t26 += v * b11;
    t27 += v * b12;
    t28 += v * b13;
    t29 += v * b14;
    t30 += v * b15;

    t0 += 38 * t16;
    t1 += 38 * t17;
    t2 += 38 * t18;
    t3 += 38 * t19;
    t4 += 38 * t20;
    t5 += 38 * t21;
    t6 += 38 * t22;
    t7 += 38 * t23;
    t8 += 38 * t24;
    t9 += 38 * t25;
    t10 += 38 * t26;
    t11 += 38 * t27;
    t12 += 38 * t28;
    t13 += 38 * t29;
    t14 += 38 * t30;
    // t15 left as is

    // first car
    c = 1;
    v = t0 + c + 65535; c = Math.floor(v / 65536); t0 = v - c * 65536;
    v = t1 + c + 65535; c = Math.floor(v / 65536); t1 = v - c * 65536;
    v = t2 + c + 65535; c = Math.floor(v / 65536); t2 = v - c * 65536;
    v = t3 + c + 65535; c = Math.floor(v / 65536); t3 = v - c * 65536;
    v = t4 + c + 65535; c = Math.floor(v / 65536); t4 = v - c * 65536;
    v = t5 + c + 65535; c = Math.floor(v / 65536); t5 = v - c * 65536;
    v = t6 + c + 65535; c = Math.floor(v / 65536); t6 = v - c * 65536;
    v = t7 + c + 65535; c = Math.floor(v / 65536); t7 = v - c * 65536;
    v = t8 + c + 65535; c = Math.floor(v / 65536); t8 = v - c * 65536;
    v = t9 + c + 65535; c = Math.floor(v / 65536); t9 = v - c * 65536;
    v = t10 + c + 65535; c = Math.floor(v / 65536); t10 = v - c * 65536;
    v = t11 + c + 65535; c = Math.floor(v / 65536); t11 = v - c * 65536;
    v = t12 + c + 65535; c = Math.floor(v / 65536); t12 = v - c * 65536;
    v = t13 + c + 65535; c = Math.floor(v / 65536); t13 = v - c * 65536;
    v = t14 + c + 65535; c = Math.floor(v / 65536); t14 = v - c * 65536;
    v = t15 + c + 65535; c = Math.floor(v / 65536); t15 = v - c * 65536;
    t0 += c - 1 + 37 * (c - 1);

    // second car
    c = 1;
    v = t0 + c + 65535; c = Math.floor(v / 65536); t0 = v - c * 65536;
    v = t1 + c + 65535; c = Math.floor(v / 65536); t1 = v - c * 65536;
    v = t2 + c + 65535; c = Math.floor(v / 65536); t2 = v - c * 65536;
    v = t3 + c + 65535; c = Math.floor(v / 65536); t3 = v - c * 65536;
    v = t4 + c + 65535; c = Math.floor(v / 65536); t4 = v - c * 65536;
    v = t5 + c + 65535; c = Math.floor(v / 65536); t5 = v - c * 65536;
    v = t6 + c + 65535; c = Math.floor(v / 65536); t6 = v - c * 65536;
    v = t7 + c + 65535; c = Math.floor(v / 65536); t7 = v - c * 65536;
    v = t8 + c + 65535; c = Math.floor(v / 65536); t8 = v - c * 65536;
    v = t9 + c + 65535; c = Math.floor(v / 65536); t9 = v - c * 65536;
    v = t10 + c + 65535; c = Math.floor(v / 65536); t10 = v - c * 65536;
    v = t11 + c + 65535; c = Math.floor(v / 65536); t11 = v - c * 65536;
    v = t12 + c + 65535; c = Math.floor(v / 65536); t12 = v - c * 65536;
    v = t13 + c + 65535; c = Math.floor(v / 65536); t13 = v - c * 65536;
    v = t14 + c + 65535; c = Math.floor(v / 65536); t14 = v - c * 65536;
    v = t15 + c + 65535; c = Math.floor(v / 65536); t15 = v - c * 65536;
    t0 += c - 1 + 37 * (c - 1);

    o[0] = t0;
    o[1] = t1;
    o[2] = t2;
    o[3] = t3;
    o[4] = t4;
    o[5] = t5;
    o[6] = t6;
    o[7] = t7;
    o[8] = t8;
    o[9] = t9;
    o[10] = t10;
    o[11] = t11;
    o[12] = t12;
    o[13] = t13;
    o[14] = t14;
    o[15] = t15;
}





let tagTypes: { [tag: number]: string } = {
    0x01: "BOOLEAN",
    0x02: "INTEGER",
    0x03: "BIT STRING",
    0x04: "OCTET STRING",
    0x05: "NULL",
    0x06: "OBJECT IDENTIFIER",
    0x0C: "UTF8String",
    0x13: "PrintableString",
    0x14: "TeletexString",
    0x16: "IA5String",
    0x17: "UTCTime",
    0x18: "GeneralizedTime",
    0x10: "SEQUENCE",
    0x11: "SET",
};
let tagLookup = {
    BOOLEAN: 0x01,
    INTEGER: 0x02,
    BIT_STRING: 0x03,
    OCTET_STRING: 0x04,
    NULL: 0x05,
    OBJECT_IDENTIFIER: 0x06,
    UTF8String: 0x0C,
    PrintableString: 0x13,
    TeletexString: 0x14,
    IA5String: 0x16,
    UTCTime: 0x17,
    GeneralizedTime: 0x18,
    SEQUENCE: 0x10,
    SET: 0x11,
};

interface BERTag {
    type: number;
    typeName: string;
    data: Buffer;
    children?: BERTag[];
}
type BERTagInput = {
    type: number;
    data: Buffer | number[];
} | {
    type: number;
    children: BERTagInput[];
};
interface Stream {
    pos: number;
    buffer: Buffer;
}
function readVarLengthNumber(stream: Stream): number {
    let value = stream.buffer[stream.pos++];
    if (value > 128) {
        let length = value - 128;
        if (length === 0) {
            throw new Error(`Indefinite length not supported`);
        }
        let bytes = stream.buffer.slice(stream.pos, stream.pos + length);
        stream.pos += length;
        value = 0;
        for (let byte of bytes) {
            value = value * 256 + byte;
        }
        return value;
    }
    return value;
}
function berTagParser(buffer: Buffer): BERTag[] {
    let stream: Stream = { pos: 0, buffer };
    // type, length, data
    let tags: BERTag[] = [];
    while (stream.pos < stream.buffer.length) {
        let type = stream.buffer[stream.pos++];
        let length = readVarLengthNumber(stream);
        let data = stream.buffer.slice(stream.pos, stream.pos + length);
        stream.pos += length;
        let tag = { type, data, typeName: "" } as BERTag;
        tags.push(tag);
        if (tag.type & 0x20) {
            tag.type -= 0x20;
            tag.children = berTagParser(data);
        }
        tag.typeName = tagTypes[tag.type] || `Unknown ${tag.type}`;
    }
    return tags;
}


function encodeVarLengthNumber(value: number): number[] {
    if (value < 128) {
        return [value];
    }
    let bytes = [];
    let value2 = value;
    while (value2) {
        bytes.push(value2 & 0xFF);
        value2 = Math.floor(value2 / 256);
    }
    bytes.reverse();
    bytes.unshift(bytes.length + 128);
    return bytes;
}

function encodeBerTags(tags: BERTagInput[]): Buffer {
    return Buffer.from(encodeBerTagsBase(tags));
}
function encodeBerTagsBase(tags: BERTagInput[]): number[] {
    let result: number[] = [];
    for (let tag of tags) {
        let type = tag.type;
        if ("children" in tag) {
            type = type | 0x20;
        }
        result.push(type);
        let data = "data" in tag ? [...tag.data] : encodeBerTagsBase(tag.children);
        result.push(...encodeVarLengthNumber(data.length));
        result.push(...data);
    }
    return result;
}