mojic.js (12773B)
1 #!/usr/bin/env node 2 3 import { program } from 'commander'; 4 import inquirer from 'inquirer'; 5 import chalk from 'chalk'; 6 import fs from 'fs'; 7 import path from 'path'; 8 import { Transform } from 'stream'; 9 import { StringDecoder } from 'string_decoder'; 10 import { CipherEngine } from '../lib/CipherEngine.js'; 11 12 const VERSION = '2.1.5'; 13 14 program 15 .name('mojic') 16 .description('Obfuscate C source code into emojis') 17 .version(VERSION) 18 .addHelpCommand('help [command]', 'Display help for command') 19 .showHelpAfterError(); 20 21 // --- Helpers --- 22 23 const promptPassword = async (msg) => { 24 const { password } = await inquirer.prompt([{ 25 type: 'password', name: 'password', message: msg, mask: '*', 26 validate: (input) => input.length > 5 || 'Password must be > 5 chars.' 27 }]); 28 return password; 29 }; 30 31 const getStreamHeader = async (filePath) => { 32 const fileStream = fs.createReadStream(filePath, { highWaterMark: 1024 }); 33 const decoder = new StringDecoder('utf8'); 34 35 return new Promise((resolve, reject) => { 36 let buffer = ''; 37 let found = false; 38 39 fileStream.on('data', (chunk) => { 40 if (found) return; 41 buffer += decoder.write(chunk); 42 const newlineIndex = buffer.indexOf('\n'); 43 if (newlineIndex !== -1) { 44 found = true; 45 fileStream.destroy(); 46 resolve(buffer.substring(0, newlineIndex)); 47 } 48 }); 49 50 fileStream.on('end', () => { 51 if (!found) resolve(buffer + decoder.end()); 52 }); 53 fileStream.on('error', reject); 54 }); 55 }; 56 57 const createHeaderSkipper = () => { 58 const decoder = new StringDecoder('utf8'); 59 let isHeaderSkipped = false; 60 let buffer = ''; 61 62 return new Transform({ 63 transform(chunk, encoding, cb) { 64 if (isHeaderSkipped) { 65 this.push(chunk); 66 return cb(); 67 } 68 69 buffer += decoder.write(chunk); 70 const nlIndex = buffer.indexOf('\n'); 71 72 if (nlIndex !== -1) { 73 const remainder = buffer.substring(nlIndex + 1); 74 this.push(remainder); 75 isHeaderSkipped = true; 76 buffer = ''; 77 } 78 cb(); 79 } 80 }); 81 }; 82 83 const createMinifier = () => { 84 return new Transform({ 85 transform(chunk, encoding, cb) { 86 let str = chunk.toString(); 87 str = str.replace(/\r?\n|\r/g, ' '); 88 str = str.replace(/\s+/g, ' '); 89 this.push(str); 90 cb(); 91 } 92 }); 93 }; 94 95 async function traverseDirectory(currentPath, extension, callback) { 96 const entries = fs.readdirSync(currentPath, { withFileTypes: true }); 97 for (const entry of entries) { 98 const fullPath = path.join(currentPath, entry.name); 99 if (entry.isDirectory()) { 100 await traverseDirectory(fullPath, extension, callback); 101 } else if (entry.isFile() && entry.name.endsWith(extension)) { 102 await callback(fullPath); 103 } 104 } 105 } 106 107 // --- Main Commands --- 108 109 program 110 .command('encode') 111 .argument('<path>', 'File or Directory to encode') 112 .option('-r, --recursive', 'Recursively encrypt all .c files in directory') 113 .option('-f, --flat', 'Flatten structure (strip whitespace/newlines) before encrypting') 114 .description('Encrypt a file or directory into emojis') 115 .action(async (targetPath, options) => { 116 try { 117 if (!fs.existsSync(targetPath)) throw new Error('Path not found'); 118 const stats = fs.statSync(targetPath); 119 120 if (stats.isDirectory() && !options.recursive) { 121 throw new Error(`'${targetPath}' is a directory. Use -r to process recursively.`); 122 } 123 124 console.log(chalk.blue(`Initiating Mojic Encryption v${VERSION}...`)); 125 if (options.flat) console.log(chalk.yellow(' -> Structural Flattening Enabled')); 126 127 const password = await promptPassword('Create password for file(s):'); 128 129 const processFile = async (filePath) => { 130 const outputName = filePath.replace(/\.c$/, '') + '.mojic'; 131 console.log(chalk.dim(` Processing: ${path.basename(filePath)} -> ${path.basename(outputName)}`)); 132 133 const engine = new CipherEngine(password); 134 await engine.init(); 135 136 const readStream = fs.createReadStream(filePath); 137 const writeStream = fs.createWriteStream(outputName); 138 139 writeStream.write(engine._encodeHeader()); 140 141 await new Promise((resolve, reject) => { 142 let pipeline = readStream; 143 if (options.flat) pipeline = pipeline.pipe(createMinifier()); 144 145 pipeline 146 .pipe(engine.getEncryptStream()) 147 .pipe(writeStream) 148 .on('finish', resolve) 149 .on('error', reject); 150 }); 151 }; 152 153 if (stats.isDirectory()) { 154 console.log(chalk.blue(`Scanning directory: ${targetPath}`)); 155 await traverseDirectory(targetPath, '.c', processFile); 156 console.log(chalk.green('Batch encryption complete.')); 157 } else { 158 await processFile(targetPath); 159 console.log(chalk.green(`Encrypted.`)); 160 } 161 162 } catch (err) { 163 console.error(chalk.red('Error:'), err.message); 164 } 165 }); 166 167 program 168 .command('decode') 169 .argument('<path>', 'File or Directory to decode') 170 .option('-r, --recursive', 'Recursively decode all .mojic files') 171 .description('Restore mojic files to C source code') 172 .action(async (targetPath, options) => { 173 try { 174 if (!fs.existsSync(targetPath)) throw new Error('Path not found'); 175 const stats = fs.statSync(targetPath); 176 177 if (stats.isDirectory() && !options.recursive) { 178 throw new Error(`'${targetPath}' is a directory. Use -r to process recursively.`); 179 } 180 181 console.log(chalk.blue('Initiating Decryption...')); 182 const password = await promptPassword('Enter password:'); 183 184 const processFile = async (filePath) => { 185 try { 186 const outputName = filePath.replace(/\.mojic$/, '') + '.restored.c'; 187 console.log(chalk.dim(` Restoring: ${path.basename(filePath)}`)); 188 189 const headerStr = await getStreamHeader(filePath); 190 const metadata = CipherEngine.decodeHeader(headerStr); 191 const engine = new CipherEngine(password); 192 await engine.init(metadata.saltHex, metadata.authCheckHex); 193 194 const readStream = fs.createReadStream(filePath); 195 const writeStream = fs.createWriteStream(outputName); 196 197 await new Promise((resolve, reject) => { 198 const decryptStream = engine.getDecryptStream(); 199 decryptStream.on('error', (err) => { 200 readStream.destroy(); // Stop reading 201 writeStream.destroy(); // Stop writing 202 reject(err); 203 }); 204 205 readStream 206 .pipe(createHeaderSkipper()) 207 .pipe(decryptStream) 208 .pipe(writeStream) 209 .on('finish', resolve) 210 .on('error', reject); 211 }); 212 return true; 213 } catch (e) { 214 const outputName = filePath.replace(/\.mojic$/, '') + '.restored.c'; 215 // Wait for stream handles to release 216 setTimeout(() => { if (fs.existsSync(outputName)) try { fs.unlinkSync(outputName); } catch(ign){} }, 100); 217 218 if (e.message === "WRONG_PASSWORD") { 219 console.log(chalk.red(` Error: Incorrect Password`)); 220 } else if (e.message === "FILE_TAMPERED") { 221 console.log(chalk.red(` Error: File Tampered! Integrity check failed.`)); 222 } else { 223 console.log(chalk.red(` Error: ${e.message}`)); 224 } 225 return false; 226 } 227 }; 228 229 if (stats.isDirectory()) { 230 console.log(chalk.blue(`Scanning directory: ${targetPath}`)); 231 await traverseDirectory(targetPath, '.mojic', processFile); 232 console.log(chalk.green('Batch decryption complete.')); 233 } else { 234 const success = await processFile(targetPath); 235 if (success) console.log(chalk.green(`Restored.`)); 236 } 237 238 } catch (err) { 239 console.error(chalk.red('Error:'), err.message); 240 } 241 }); 242 243 // --- SRT --- 244 const rotatePassword = async (file) => { 245 try { 246 if (!fs.existsSync(file)) throw new Error('File not found'); 247 console.log(chalk.yellow(`Rotating Password for ${path.basename(file)}...`)); 248 249 const headerStr = await getStreamHeader(file); 250 const metadata = CipherEngine.decodeHeader(headerStr); 251 const oldPass = await promptPassword('Enter CURRENT password:'); 252 253 const oldEngine = new CipherEngine(oldPass); 254 await oldEngine.init(metadata.saltHex, metadata.authCheckHex); 255 256 const newPass = await promptPassword('Enter NEW password:'); 257 const newEngine = new CipherEngine(newPass); 258 await newEngine.init(); 259 260 const tempFile = file + '.tmp'; 261 const readStream = fs.createReadStream(file); 262 const writeStream = fs.createWriteStream(tempFile); 263 264 writeStream.write(newEngine._encodeHeader()); 265 266 await new Promise((resolve, reject) => { 267 const decryptStream = oldEngine.getDecryptStream(); 268 decryptStream.on('error', reject); 269 readStream.pipe(createHeaderSkipper()).pipe(decryptStream).pipe(newEngine.getEncryptStream()).pipe(writeStream).on('finish', resolve).on('error', reject); 270 }); 271 272 fs.renameSync(tempFile, file); 273 console.log(chalk.green(`Password updated.`)); 274 275 } catch (err) { 276 if (fs.existsSync(file + '.tmp')) fs.unlinkSync(file + '.tmp'); 277 if (err.message === "WRONG_PASSWORD") console.error(chalk.red('Error: Incorrect Current Password')); 278 else console.error(chalk.red('Error:'), err.message); 279 } 280 }; 281 282 const reEncrypt = async (file) => { 283 try { 284 if (!fs.existsSync(file)) throw new Error('File not found'); 285 console.log(chalk.yellow(`Re-shuffling Entropy for ${path.basename(file)}...`)); 286 287 const headerStr = await getStreamHeader(file); 288 const metadata = CipherEngine.decodeHeader(headerStr); 289 const password = await promptPassword('Enter password:'); 290 291 const oldEngine = new CipherEngine(password); 292 await oldEngine.init(metadata.saltHex, metadata.authCheckHex); 293 294 const newEngine = new CipherEngine(password); 295 await newEngine.init(); 296 297 const tempFile = file + '.tmp'; 298 const readStream = fs.createReadStream(file); 299 const writeStream = fs.createWriteStream(tempFile); 300 301 writeStream.write(newEngine._encodeHeader()); 302 303 await new Promise((resolve, reject) => { 304 const decryptStream = oldEngine.getDecryptStream(); 305 decryptStream.on('error', reject); 306 readStream.pipe(createHeaderSkipper()).pipe(decryptStream).pipe(newEngine.getEncryptStream()).pipe(writeStream).on('finish', resolve).on('error', reject); 307 }); 308 309 fs.renameSync(tempFile, file); 310 console.log(chalk.green(`File re-encrypted.`)); 311 312 } catch (err) { 313 if (fs.existsSync(file + '.tmp')) fs.unlinkSync(file + '.tmp'); 314 if (err.message === "WRONG_PASSWORD") console.error(chalk.red('Error: Incorrect Password')); 315 else console.error(chalk.red('Error:'), err.message); 316 } 317 }; 318 319 program.command('srt').description('Security and Rotation Tools').option('--pass <file>', 'Update password').option('--re <file>', 'Re-encrypt').action(async (options) => { 320 if (options.pass) await rotatePassword(options.pass); 321 else if (options.re) await reEncrypt(options.re); 322 else { console.log(chalk.yellow('Please specify an option: --pass <file> or --re <file>')); program.commands.find(c => c.name() === 'srt').help(); } 323 }); 324 325 program.parse(process.argv);