"use strict";
const fs = require('fs');
const JavaScriptObfuscator = require('javascript-obfuscator');
const KeyPadCode = 94;
const randomkey = (len) => (new Array(len)).fill(0).map(v => Math.round(Math.random() * 0xffffff));
// MARK: keys
function parseKeysFile() {
    function parse(line) {
        const re = /^\s*?(.*?)\s*?:\s*?(.*?)(?:$|\s*?#\s*?(\d+)\s*?,\s*?(.*?)\s*?$)/.exec(line);
        if (!re || re.length < 3 || !re[1]) {
            throw "Cannot Parse";
        }
        let name = re[1].trim(), data = re[2].trim(), offset, obfuscatedname;
        if (re.length < 5 || re[3] === undefined) {
            offset = Math.round(Math.random() * (data.length - 2) + 1);
            // add a random multiple of data.length, no effect but may obfuscate the shift
            offset += data.length * Math.round(Math.random() * 4);
            obfuscatedname = '_0x' + Math.round(Math.random() * 0xffffff).toString(16);
        }
        else {
            offset = parseInt(re[3].trim());
            if (isNaN(offset)) {
                throw 'offset is NaN';
            }
            obfuscatedname = re[4].trim();
        }
        return {
            name, data, offset, obfuscatedname,
            cleanLine: `${name}: ${data} # ${offset}, ${obfuscatedname}`
        };
    }
    function createscript({ name, data, offset }) {
        const k = randomkey(data.length);
        const d = data.split('').map((ch, i) => {
            const kk = k[(i + offset) % data.length];
            if (ch === '*') {
                return KeyPadCode + kk;
            }
            return ch.charCodeAt(0) + kk;
        });
        return `function ${name}key() { return ${JSON.stringify(k)} }
function ${name}data() { return ${JSON.stringify(d)} }
function ${name}check(str) { return str === getdata(${name}key, ${name}data, ${offset}) }`;
    }
    const file = fs.readFileSync('./src/flags').toString();
    const filelines = file.split('\n');
    let newfile = '';
    let scriptfile = '';
    let names = [];
    for (let i = 0; i < filelines.length; i++) {
        const line = filelines[i];
        if (line.trim().length === 0) {
            continue;
        }
        if (line.startsWith('#!')) {
            continue;
        }
        try {
            const parsed = parse(line);
            newfile += parsed.cleanLine + '\n';
            scriptfile += createscript(parsed) + '\n';
            names.push([`${parsed.name}check`, parsed.obfuscatedname]);
        }
        catch (e) {
            throw `Error on line ${i + 1}: ${e}`;
        }
    }
    fs.writeFileSync('./src/flags', newfile);
    fs.writeFileSync('./out/debug/flags', newfile);
    return { scriptfile, names };
}
// MARK: hints
function parseHints() {
    function parse(line) {
        const re = /^(.*?):\s*?(.*?)(?:$|\s*?;\s*?(.*?)(?:$|\s*?#\s*?(\d+)\s*?,\s*?(.*?)\s*?$))/.exec(line);
        if (!re || re.length < 3 || re[1] === undefined) {
            throw "Cannot Parse";
        }
        let name = re[1].trim(), hintraw = re[2].trim(), alertraw = (re[3] ?? '').trim(), offset, obfuscatedname;
        let hint = hintraw.replaceAll('\\n', '\n');
        let alert = alertraw.replaceAll('\\n', '\n');
        let data = btoa(JSON.stringify([hint, alert]));
        if (re.length < 6 || !re[4]) {
            offset = Math.round(Math.random() * (data.length - 2) + 1);
            // add a random multiple of data.length, no effect but may obfuscate the shift
            offset += data.length * Math.round(Math.random() * 4);
            obfuscatedname = '_0x' + Math.round(Math.random() * 0xffffff).toString(16);
        }
        else {
            offset = parseInt(re[4].trim());
            if (isNaN(offset)) {
                throw 'offset is NaN';
            }
            obfuscatedname = re[5].trim();
        }
        return {
            name, data, offset, obfuscatedname,
            cleanLine: `${name}: ${hintraw} ; ${alertraw} # ${offset}, ${obfuscatedname}`
        };
    }
    function createscript({ name, data, offset }) {
        const k = randomkey(data.length);
        const d = data.split('').map((ch, i) => {
            const kk = k[(i + offset) % data.length];
            if (ch === '*') {
                return KeyPadCode + kk;
            }
            return ch.charCodeAt(0) + kk;
        });
        return `function h_${name}key() { return ${JSON.stringify(k)} }
function h_${name}data() { return ${JSON.stringify(d)} }
function hint_${name}() { return getdata(h_${name}key, h_${name}data, ${offset}) }`;
    }
    const file = fs.readFileSync('./src/hints').toString();
    const filelines = file.split('\n');
    let newfile = '';
    let scriptfile = '';
    let names = [];
    for (let i = 0; i < filelines.length; i++) {
        const line = filelines[i];
        if (line.trim().length === 0 || line.startsWith('#')) {
            continue;
        }
        try {
            const parsed = parse(line);
            newfile += parsed.cleanLine + '\n';
            scriptfile += createscript(parsed) + '\n';
            names.push([`hint_${parsed.name}`, parsed.obfuscatedname]);
        }
        catch (e) {
            throw `Error on line ${i + 1}: ${e}`;
        }
    }
    fs.writeFileSync('./src/hints', newfile);
    fs.writeFileSync('./out/debug/hints', newfile);
    return { scriptfile, names };
}
// MARK: obfuscate
function obfuscateCore(core, reservedNames) {
    return JavaScriptObfuscator.obfuscate(core, {
        optionsPreset: 'default',
        seed: 7,
        target: 'browser',
        controlFlowFlattening: true,
        controlFlowFlatteningThreshold: 1,
        deadCodeInjection: true,
        deadCodeInjectionThreshold: 1,
        identifierNamesGenerator: 'hexadecimal',
        numbersToExpressions: true,
        renameGlobals: true,
        renameProperties: true,
        renamePropertiesMode: 'safe',
        reservedNames,
        stringArray: false,
        transformObjectKeys: true,
    }).getObfuscatedCode();
}
function obfuscateMain(mainjs) {
    const file = fs.readFileSync('./src/core.js').toString();
    mainjs += '\n' + file;
    return JavaScriptObfuscator.obfuscate(mainjs, {
        optionsPreset: 'high-obfuscation',
        seed: 13,
        target: 'browser',
        controlFlowFlattening: true,
        controlFlowFlatteningThreshold: 0.9,
        deadCodeInjection: true,
        deadCodeInjectionThreshold: 0.8,
        debugProtection: true,
        debugProtectionInterval: 2000,
        disableConsoleOutput: true,
        identifierNamesGenerator: 'hexadecimal',
        numbersToExpressions: true,
        renameGlobals: true,
        renameProperties: true,
        renamePropertiesMode: 'safe',
        selfDefending: true,
        simplify: true,
        splitStrings: true,
        splitStringsChunkLength: 4,
        stringArray: true,
        stringArrayCallsTransform: true,
        stringArrayCallsTransformThreshold: 0.9,
        stringArrayEncoding: ['rc4'],
        stringArrayIndexesType: ['hexadecimal-number', 'hexadecimal-numeric-string'],
        stringArrayWrappersCount: 1,
        stringArrayWrappersParametersMaxCount: 3,
        stringArrayWrappersType: 'function',
        stringArrayThreshold: 0.9,
        transformObjectKeys: true,
        log: true
        //domainLock
    }).getObfuscatedCode();
}
function doswap(b64) {
    function swap(b64, c1, c2) { return b64.replaceAll(c1, '#').replaceAll(c2, c1).replaceAll('#', c2); }
    b64 = swap(b64, '8', 'f');
    b64 = swap(b64, 'W', '2');
    b64 = swap(b64, 'M', 'C');
    b64 = swap(b64, 'z', 'm');
    b64 = swap(b64, 'a', 'S');
    b64 = swap(b64, 'H', 'l');
    b64 = swap(b64, 'e', 'Y');
    return b64;
}
// MARK: templating
function loadMainjs(coreObfuscatedB64swap) {
    const file = fs.readFileSync('./src/main.js').toString();
    return file.replace('{{TEXT}}', coreObfuscatedB64swap);
}
function loadDesign(script) {
    const file = fs.readFileSync('./src/design.html').toString();
    const split = file.split('{{TEXT}}');
    return split[0] + script + split[1];
    //return file.replace('{{TEXT}}', script)
}
// MARK: main
function main() {
    if (fs.existsSync('./out')) {
        fs.rmSync('./out', { recursive: true, force: true });
    }
    fs.mkdirSync('./out');
    fs.mkdirSync('./out/debug');
    console.log('Parsing keys');
    const keysFile = parseKeysFile();
    console.log('Parsing hints');
    const hintsFile = parseHints();
    const renames = [
        ["getFinalImage", "_0xc65ffb"],
        ...keysFile.names,
        ...hintsFile.names
    ];
    console.log('Loading core.js');
    const file = fs.readFileSync('./src/core.js').toString();
    const scriptfile = keysFile.scriptfile + '\n' + hintsFile.scriptfile + '\n' + file;
    fs.writeFileSync('./out/debug/core.js', scriptfile);
    console.log('Obfuscating core.js');
    let coreObfuscated = obfuscateCore(scriptfile, [...renames.map(v => v[0])]);
    for (let name of renames) {
        coreObfuscated = coreObfuscated.replace(`function ${name[0]}(`, `function ${name[1]}(`);
    }
    coreObfuscated += `function dear_osint_hater() { return "You made it so far! The end is almost here. Since you decided to destroy my hard work, you will not reach the final prize the normal way, so here you go: https://zomo.land/hosted/startrekctf.png. Good luck finding the flag!" }`;
    fs.writeFileSync('./out/debug/core.obfuscated', coreObfuscated);
    console.log('Converting core.js to base64');
    const coreObfuscatedB64 = btoa(coreObfuscated);
    fs.writeFileSync('./out/debug/core.obfuscated.base64', coreObfuscatedB64);
    console.log('Obfuscating core.js base64');
    const coreObfuscatedB64swap = doswap(coreObfuscatedB64);
    fs.writeFileSync('./out/debug/core.obfuscated.base64.obfuscated', coreObfuscatedB64swap);
    console.log('Loading main.js');
    const mainjs = loadMainjs(coreObfuscatedB64swap);
    fs.writeFileSync('./out/debug/main.js', mainjs);
    console.log('Creating nonobfuscated.html');
    const mainhtml = loadDesign(mainjs);
    fs.writeFileSync('./out/debug/nonobfuscated.html', mainhtml);
    if (process.argv[2] === 'dev') {
        console.log('Flag dev: skipping obfuscation');
        return;
    }
    console.log('Obfuscating main.js');
    const mainjsObfuscated = obfuscateMain(mainjs);
    fs.writeFileSync('./out/debug/main.obfuscated', mainjsObfuscated);
    console.log('Creating file.html');
    const mainhtmlObfuscated = loadDesign(mainjsObfuscated);
    fs.writeFileSync('./out/debug/final.html', mainhtmlObfuscated);
    fs.writeFileSync('./out/4LostInSpace.html', mainhtmlObfuscated);
}
main();
