UNPKG

@zozzona/js

Version:

Advanced source protection toolkit (obfuscate → minify → encrypt) with reversible unpacking and git-safe automation.

236 lines (194 loc) 7.57 kB
#!/usr/bin/env node import fs from 'fs-extra'; import path from 'path'; import { glob } from 'glob'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); /* ---------------------------------------------------------- UTF-8 BOM REMOVAL ---------------------------------------------------------- */ function stripBOM(str) { if (!str) return str; return str.replace(/^\uFEFF/, ""); } /* PACK_INCLUDE / PACK_IGNORE - FIXED to handle pipe delimiter */ function buildPatternFromEnv() { const inc = process.env.PACK_INCLUDE ? process.env.PACK_INCLUDE.split('|') : []; const ign = process.env.PACK_IGNORE ? process.env.PACK_IGNORE.split('|') : []; // Filter out empty patterns const patterns = inc.filter(p => p && p.trim()); // If we have include patterns, return them as array or single pattern let pattern; if (patterns.length === 0) { pattern = null; } else if (patterns.length === 1) { pattern = patterns[0]; } else { // Return array of patterns for glob to handle pattern = patterns; } // Set ignore patterns (keep pipe delimiter for consistency) process.env.OBF_IGNORE = ign.join('|'); console.log(`📋 OBF Pattern: ${JSON.stringify(pattern)}`); console.log(`🚫 OBF Ignore: ${process.env.OBF_IGNORE}`); return pattern; } const CONFIG_PATTERN = buildPatternFromEnv(); const parser = require('@babel/parser'); const traversePkg = require('@babel/traverse'); const generatePkg = require('@babel/generator'); const traverse = traversePkg.default || traversePkg; const generate = generatePkg.default || generatePkg; const DEFAULT_GLOB = 'src/**/*.{js,jsx,ts,tsx}'; const MAP_FILE = 'obfuscation-map.json'; /* UPDATED — ignore JSON, package.json */ async function readAllFiles(globPattern) { const ignoreList = [ "**/node_modules/**", "**/*.json", "**/package.json" ]; if (process.env.OBF_IGNORE) { ignoreList.push(...process.env.OBF_IGNORE.split('|')); } return await glob(globPattern, { nodir: true, ignore: ignoreList }); } function parseCode(code, filename) { const isTS = filename.endsWith('.ts') || filename.endsWith('.tsx'); return parser.parse(code, { sourceType: 'unambiguous', plugins: [ 'jsx', isTS ? 'typescript' : null, 'classProperties', 'optionalChaining', 'nullishCoalescingOperator', 'objectRestSpread', 'decorators-legacy' ].filter(Boolean) }); } const BLACKLIST = new Set([ 'require','module','exports','console','window','document','global', '__dirname','__filename','process','Buffer','setTimeout','setInterval', 'clearTimeout','clearInterval','Promise' ]); const BASE62 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; function toBase62(num){ if(num===0) return '0'; let s=''; while(num>0){ s=BASE62[num%62]+s; num=Math.floor(num/62);} return s; } function makeObfName(counter,prefix='_r'){ return `${prefix}${toBase62(counter)}`; } /* ---------------------------------------------------------- OBFUSCATE ---------------------------------------------------------- */ async function obfuscate(globPattern = DEFAULT_GLOB, mapFile = MAP_FILE) { const files = await readAllFiles(globPattern); if (!files.length) { console.log(`No files matched: ${globPattern}`); return; } const mapping = (await fs.pathExists(mapFile)) ? await fs.readJson(mapFile) : {}; let counter = Object.keys(mapping).length + 1; function genName(original) { if (mapping[original]) return mapping[original]; const n = makeObfName(counter++); mapping[original] = n; return n; } // Progress indicator const totalFiles = files.length; let processedCount = 0; for (const file of files) { processedCount++; const progress = `[${processedCount}/${totalFiles}]`; // Show progress with current file process.stdout.write(`\r⚙️ ${progress} Obfuscating: ${file}...`.substring(0, 100).padEnd(100)); const abs = path.resolve(file); // ⭐ Strip BOM before parsing let code = await fs.readFile(abs, 'utf8'); code = stripBOM(code); let ast; try { ast = parseCode(code, file); } catch (e) { process.stdout.write('\r' + ' '.repeat(100) + '\r'); console.warn(`Parse error in ${file}: ${e.message}`); continue; } traverse(ast, { enter(path) { const bindings = path.scope?.bindings || {}; for (const name of Object.keys(bindings)) { if (BLACKLIST.has(name)) continue; if (name.startsWith('_r')) continue; const newName = genName(name); try { path.scope.rename(name, newName); } catch (e) { /* ignore rename failures */ } } }, ImportSpecifier(p){ renameImportLike(p, genName); }, ImportDefaultSpecifier(p){ renameImportLike(p, genName); }, ExportSpecifier(p){ renameImportLike(p, genName); } }); const out = generate(ast, { comments: true }, code).code; await fs.writeFile(abs, out, 'utf8'); // Clear progress line and show completion process.stdout.write('\r' + ' '.repeat(100) + '\r'); console.log(`✓ Obfuscated ${file}`); } await fs.writeJson(mapFile, mapping, { spaces: 2 }); console.log(`\n✔ Obfuscation complete: ${totalFiles} files processed`); } function renameImportLike(path, genName){ const local = path.node.local?.name; if (local && !BLACKLIST.has(local) && !local.startsWith('_r')) { path.node.local.name = genName(local); } } /* ---------------------------------------------------------- DEOBFUSCATE ---------------------------------------------------------- */ async function deobfuscate(globPattern = DEFAULT_GLOB, mapFile = MAP_FILE) { if (!await fs.pathExists(mapFile)) throw new Error(`${mapFile} missing`); const mapping = await fs.readJson(mapFile); const inv = Object.fromEntries(Object.entries(mapping).map(([a,b])=>[b,a])); const files = await readAllFiles(globPattern); if (!files.length) return; for (const file of files) { const abs = path.resolve(file); // ⭐ Strip BOM before parsing let code = await fs.readFile(abs, 'utf8'); code = stripBOM(code); let ast; try { ast = parseCode(code, file); } catch (e){ console.warn(`Parse error ${file}`); continue; } traverse(ast, { enter(path){ const bindings = path.scope?.bindings || {}; for (const name of Object.keys(bindings)) { if (!inv[name]) continue; try { path.scope.rename(name, inv[name]); } catch (e){ console.warn(`Failed rename ${name}`); } } }, ImportSpecifier(p){ unrenameImportLike(p, inv); }, ImportDefaultSpecifier(p){ unrenameImportLike(p, inv); }, ExportSpecifier(p){ unrenameImportLike(p, inv); } }); const out = generate(ast, { comments: true }, code).code; await fs.writeFile(abs, out, 'utf8'); console.log(`Deobfuscated ${file}`); } } function unrenameImportLike(path, inv){ const local = path.node.local?.name; if (local && inv[local]) path.node.local.name = inv[local]; } /* ---------------------------------------------------------- MAIN ---------------------------------------------------------- */ async function main() { const cmd = process.argv[2] || 'obfuscate'; const pattern = CONFIG_PATTERN || process.env.OBF_GLOB || DEFAULT_GLOB; if (cmd === 'obfuscate') await obfuscate(pattern, MAP_FILE); else if (cmd === 'deobfuscate') await deobfuscate(pattern, MAP_FILE); else console.log('Use obfuscate|deobfuscate'); } main();