jsobx
Version:
a simple js obfuscator
269 lines (223 loc) • 7.72 kB
JavaScript
const acorn = require('acorn');
const walk = require('acorn-walk');
const codegen = require('escodegen');
const through = require('through2');
const fs = require('fs');
const path = require('path');
const domProps = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'domprops.json'))).props;
function shuffle(array) {
for (let i = array.length; i; i--) {
let j = Math.floor(Math.random() * i);
let x = array[i - 1];
array[i - 1] = array[j];
array[j] = x;
}
}
function numberToBase52(num) {
if (num === 0) return 'a';
let result = '';
do {
const index = num % 52;
const char = String.fromCharCode(index < 26 ? index + 97 : index - 26 + 65);
result = char + result;
num = Math.floor(num / 52);
} while (num > 0);
return result;
}
const defaultOptions = {
groupDomProperties: true,
groupDomMethodsNoArgs: true,
groupDomMethods: true,
groupStrings: true,
groupRegexes: true,
shuffle: true,
mangle: false,
wrap: true,
prefix: '_',
splitString: true,
splitStringRegex: /(<\/\S>|\S+(?==)|=[^>\s]+|\n|\s{4})/
};
function process(code, options) {
options = Object.assign({}, defaultOptions, options || {});
const collectedData = new Map();
let counter = 0;
const collectData = (node) => {
let key;
if (node.type === 'BinaryExpression') {
key = JSON.stringify(node);
} else {
key = node.regex ? node.raw : node.value;
}
if (collectedData.has(key)) {
return collectedData.get(key).name;
}
const name = options.prefix + (options.mangle ? numberToBase52(counter++) : counter++);
collectedData.set(key, {
name, node
});
return name;
};
const createIdentifier = (name) => {
return {
type: 'Identifier',
name: name
};
};
const createBinaryExpression = (vars) => {
if (vars.length === 1) {
return createIdentifier(vars[0]);
}
return {
type: 'BinaryExpression',
operator: '+',
left: createBinaryExpression(vars.slice(0, -1)),
right: {
type: 'Identifier',
name: vars[vars.length - 1]
}
};
}
const comments = [];
const tokens = [];
const ast = acorn.parse(code, {
ecmaVersion: 2020,
allowReturnOutsideFunction: true,
locations: true,
ranges: true,
onComment: comments,
onToken: tokens
});
const attachedAst = codegen.attachComments(ast, comments, tokens);
walk.ancestor(ast, {
Literal(node, ancestors) {
const parent = ancestors[ancestors.length - 2];
// console.log('-- Literal --');
// console.log(node);
// console.log('parent', parent);
let transform = false;
let transformString = false;
if (options.groupStrings
&& typeof node.value === 'string' && node.value !== '' && node.value !== 'use strict'
&& !(parent.type === 'ImportDeclaration'
|| parent.type === 'CallExpression' && parent.callee.name === 'require'
|| parent.type === 'MemberExpression' && parent.property === node
)
) {
transform = true;
transformString = true;
}
if (options.groupRegexes && node.regex) {
transform = true;
}
const getReplacement = () => {
if (transformString && options.splitString) {
const parts = node.value.split(options.splitStringRegex);
const names = [];
parts.forEach((value) => {
if (value === '') return;
names.push(collectData({
type: 'Literal',
value: value
}))
});
if (names.length > 1) {
return createIdentifier(collectData(createBinaryExpression(names)));
}
}
return createIdentifier(collectData(node));
};
if (transform) {
const obj = parent.arguments || parent.elements || parent;
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
if (obj[keys[i]] === node) {
obj[keys[i]] = getReplacement();
break;
}
}
}
},
MemberExpression(node, ancestors) {
const parent = ancestors[ancestors.length - 2];
// console.log('-- MemberExpression --');
// console.log(node);
// console.log('parent', parent);
let transform = false;
if (options.groupDomMethodsNoArgs
&& parent.type === 'CallExpression'
&& !parent.arguments.length
) {
transform = true;
}
if (options.groupDomMethods
&& parent.type === 'CallExpression'
&& parent.callee.type === 'MemberExpression'
&& parent.callee.object === node.object
) {
transform = true;
}
if (options.groupDomProperties
&& (parent.type !== 'CallExpression'
|| parent.arguments && parent.arguments.indexOf(node) > -1
)
) {
transform = true;
}
if (transform) {
if (node.property.type === 'Literal'
&& node.computed
&& domProps.indexOf(node.property.value) > -1
) {
node.computed = true;
node.property = createIdentifier(collectData(node.property));
}
else if (node.property.type === 'Identifier'
&& !node.computed
&& domProps.indexOf(node.property.name) > -1
) {
node.computed = true;
node.property = createIdentifier(collectData({
type: 'Literal',
value: node.property.name
}));
}
}
}
});
if (collectedData.size > 0) {
const array = Array.from(collectedData.entries());
if (options.shuffle && !options.splitString) {
shuffle(array);
}
const declaration = {
type: 'VariableDeclaration',
kind: 'var',
declarations: array.map(([, meta]) => ({
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: meta.name
},
init: meta.node
}))
};
ast.body.unshift(declaration);
}
let output = codegen.generate(attachedAst, {
comment: true
});
if (options.wrap) {
output = `(function(){\n${output}\n})();`
}
return output;
}
function jsobx(options) {
return through.obj(function(file, encoding, callback) {
var content = String(file.contents);
content = process(content, options);
file.contents = Buffer.from(content);
return callback(null, file);
});
}
jsobx.process = process;
module.exports = jsobx;