CipherEngine.js (16703B)
1 import crypto from 'crypto'; 2 import { Transform } from 'stream'; 3 import { StringDecoder } from 'string_decoder'; 4 5 /** 6 * MOJIC v2.1.5 CIPHER ENGINE 7 * "Operation Ironclad" 8 * * Security Upgrades: 9 * - KDF: Upgraded from PBKDF2 to Scrypt (Memory-Hard, GPU-Resistant) 10 * - PRNG: Upgraded from Xoshiro256** to AES-256-CTR (Cryptographically Secure) 11 * - Auth: Extended Key material for higher entropy 12 * * Fixes: 13 * - Polymorphic Data: Added XOR Whitening to raw data chunks to hide patterns (e.g., repeating whitespace). 14 * - RNG Buffering: Fixed potential byte loss in AES-CTR buffer refill. 15 */ 16 17 // --- EMOJI UNIVERSE GENERATION --- 18 const HEADER_ALPHABET = ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗']; 19 20 const generateUniverse = () => { 21 const universe = []; 22 const ranges = [ 23 [0x1F600, 0x1F64F], // Emoticons 24 [0x1F300, 0x1F5FF], // Misc Symbols (Contains modifiers) 25 [0x1F680, 0x1F6FF], // Transport 26 [0x1F900, 0x1F9FF] // Supplemental 27 ]; 28 29 const headerSet = new Set(HEADER_ALPHABET); 30 31 for (const [start, end] of ranges) { 32 for (let code = start; code <= end; code++) { 33 const char = String.fromCodePoint(code); 34 if (!headerSet.has(char)) { 35 universe.push(char); 36 } 37 } 38 } 39 return universe; 40 }; 41 42 const RAW_UNIVERSE = generateUniverse(); 43 44 if (RAW_UNIVERSE.length < 1080) { 45 throw new Error("Critical: Emoji Universe generation failed to produce enough tokens."); 46 } 47 48 const C_KEYWORDS = [ 49 'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do', 50 'double', 'else', 'enum', 'extern', 'float', 'for', 'goto', 'if', 51 'int', 'long', 'register', 'return', 'short', 'signed', 'sizeof', 'static', 52 'struct', 'switch', 'typedef', 'union', 'unsigned', 'void', 'volatile', 'while', 53 'include', 'define', 'main', 'printf', 'NULL', '#include', '#define' 54 ]; 55 56 // --- PRNG: AES-256-CTR (CSPRNG) --- 57 // Replaces Xoshiro with a cryptographically secure stream 58 class AesCounterRNG { 59 constructor(key, iv) { 60 if (key.length !== 32 || iv.length !== 16) throw new Error("Invalid seed length for AES-RNG"); 61 this.cipher = crypto.createCipheriv('aes-256-ctr', key, iv); 62 this.buffer = Buffer.alloc(0); 63 64 // Encrypt an initial block of zeros to start the keystream 65 this._refill(); 66 } 67 68 _refill() { 69 // Generate 1KB of random keystream at a time 70 const zeros = Buffer.alloc(1024); 71 const newBytes = this.cipher.update(zeros); 72 // Correctly concatenate new bytes to existing buffer to prevent data loss 73 this.buffer = Buffer.concat([this.buffer, newBytes]); 74 } 75 76 next() { 77 if (this.buffer.length < 8) this._refill(); 78 79 // Read 64 bits (BigInt) to match previous interface 80 const val = this.buffer.readBigUInt64BE(0); 81 this.buffer = this.buffer.subarray(8); 82 return val; 83 } 84 85 nextBytes(length) { 86 if (this.buffer.length < length) this._refill(); 87 88 // Return a Buffer of requested length 89 const bytes = this.buffer.subarray(0, length); 90 this.buffer = this.buffer.subarray(length); 91 return bytes; 92 } 93 94 nextFloat() { 95 // Convert uint64 to double [0, 1) 96 // We take upper 53 bits for standard double precision 97 const val = Number(this.next() >> 11n); 98 return val * (2 ** -53); 99 } 100 } 101 102 export class CipherEngine { 103 constructor(password) { 104 this.password = password; 105 this.keywordMap = new Map(); 106 this.keywordReverseMap = new Map(); 107 this.dataAlphabet = []; 108 this.dataReverseMap = new Map(); 109 this.isReady = false; 110 this.hmac = null; 111 this.lineLength = 0; 112 } 113 114 async init(existingSaltHex = null, expectedAuthCheck = null) { 115 this.salt = existingSaltHex 116 ? Buffer.from(existingSaltHex, 'hex') 117 : crypto.randomBytes(32); // Increased salt size to 32 bytes 118 119 // SECURITY UPGRADE: Scrypt instead of PBKDF2 120 // N=16384, r=8, p=1 are standard secure defaults 121 const derivedKey = await new Promise((resolve, reject) => { 122 crypto.scrypt(this.password, this.salt, 80, { N: 16384, r: 8, p: 1 }, (err, key) => { 123 if (err) reject(err); else resolve(key); 124 }); 125 }); 126 127 // Split 80 bytes of key material: 128 // 0-32: AES Key (32 bytes) 129 // 32-48: AES IV (16 bytes) 130 // 48-80: HMAC Auth Key (32 bytes) 131 132 const rngKey = derivedKey.subarray(0, 32); 133 const rngIv = derivedKey.subarray(32, 48); 134 this.authKey = derivedKey.subarray(48, 80); 135 136 if (expectedAuthCheck) { 137 const calculatedAuthCheck = this.authKey.subarray(0, 4).toString('hex'); 138 if (calculatedAuthCheck !== expectedAuthCheck) { 139 throw new Error("WRONG_PASSWORD"); 140 } 141 } 142 143 // Initialize CSPRNG 144 this.rng = new AesCounterRNG(rngKey, rngIv); 145 146 const shuffled = this._shuffleArray([...RAW_UNIVERSE]); 147 148 let ptr = 0; 149 this.keywordEmojis = []; 150 for (const kw of C_KEYWORDS) { 151 const emo = shuffled[ptr++]; 152 this.keywordEmojis.push(emo); 153 this.keywordMap.set(kw, emo); 154 this.keywordReverseMap.set(emo, kw); 155 } 156 157 this.dataAlphabet = shuffled.slice(ptr, ptr + 1024); 158 if (this.dataAlphabet.length < 1024) throw new Error("Not enough emojis for Base-1024"); 159 160 this.dataAlphabet.forEach((emo, idx) => { 161 this.dataReverseMap.set(emo, idx); 162 }); 163 164 this.hmac = crypto.createHmac('sha256', this.authKey); 165 this.isReady = true; 166 } 167 168 _shuffleArray(array) { 169 // Fisher-Yates shuffle using CSPRNG 170 for (let i = array.length - 1; i > 0; i--) { 171 const j = Math.floor(this.rng.nextFloat() * (i + 1)); 172 [array[i], array[j]] = [array[j], array[i]]; 173 } 174 return array; 175 } 176 177 _encodeHeader() { 178 const saltHex = this.salt.toString('hex'); 179 const authCheck = this.authKey.subarray(0, 4).toString('hex'); 180 181 let headerStr = ''; 182 for (const char of (saltHex + authCheck)) { 183 const val = parseInt(char, 16); 184 headerStr += HEADER_ALPHABET[val]; 185 } 186 return headerStr + '\n'; 187 } 188 189 static decodeHeader(headerStr) { 190 let hexString = ''; 191 const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' }); 192 const segments = segmenter.segment(headerStr.trim()); 193 194 for (const { segment } of segments) { 195 const index = HEADER_ALPHABET.indexOf(segment); 196 if (index === -1) throw new Error("Invalid Header format."); 197 hexString += index.toString(16); 198 } 199 200 // Salt is now variable length (usually 32 bytes = 64 hex chars), 201 // AuthCheck is always last 4 bytes (8 hex chars) 202 const totalLen = hexString.length; 203 if (totalLen < 8) throw new Error("Header too short"); 204 205 const authStart = totalLen - 8; 206 207 return { 208 saltHex: hexString.substring(0, authStart), 209 authCheckHex: hexString.substring(authStart) 210 }; 211 } 212 213 // --- STREAMING ENCRYPTION --- 214 215 getEncryptStream() { 216 if (!this.isReady) throw new Error("Engine not initialized"); 217 const engine = this; 218 let buffer = Buffer.alloc(0); 219 220 return new Transform({ 221 transform(chunk, encoding, callback) { 222 const str = chunk.toString('utf8'); 223 224 const alphaKeywords = C_KEYWORDS.filter(k => /^\w+$/.test(k)).sort((a,b)=>b.length-a.length).join('|'); 225 const symKeywords = C_KEYWORDS.filter(k => !/^\w+$/.test(k)).map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'); 226 227 const regex = new RegExp(`(\\b(?:${alphaKeywords})\\b|(?:${symKeywords}))`, 'g'); 228 229 const parts = str.split(regex); 230 231 for (const part of parts) { 232 if (!part) continue; 233 234 if (C_KEYWORDS.includes(part)) { 235 if (buffer.length > 0) { 236 this.push(engine._flushDataBuffer(buffer)); 237 buffer = Buffer.alloc(0); 238 } 239 240 const baseIdx = engine.keywordEmojis.indexOf(engine.keywordMap.get(part)); 241 const shift = Number(engine.rng.next() % BigInt(engine.keywordEmojis.length)); 242 const newIdx = (baseIdx + shift) % engine.keywordEmojis.length; 243 const polyEmoji = engine.keywordEmojis[newIdx]; 244 245 const outBuf = Buffer.from(polyEmoji); 246 engine.hmac.update(outBuf); 247 this.push(engine._wrapOutput(outBuf)); 248 249 } else { 250 buffer = Buffer.concat([buffer, Buffer.from(part, 'utf8')]); 251 252 while (buffer.length >= 5) { 253 const chunk = buffer.subarray(0, 5); 254 buffer = buffer.subarray(5); 255 256 // XOR Whitening: Hide patterns (like spaces) by XORing with RNG stream 257 const mask = engine.rng.nextBytes(5); 258 const maskedChunk = Buffer.alloc(5); 259 for(let i=0; i<5; i++) maskedChunk[i] = chunk[i] ^ mask[i]; 260 261 const enc = engine._encodeBase1024(maskedChunk); 262 engine.hmac.update(enc); 263 this.push(engine._wrapOutput(enc)); 264 } 265 } 266 } 267 callback(); 268 }, 269 270 flush(callback) { 271 if (buffer.length > 0) { 272 const padded = Buffer.alloc(5); 273 buffer.copy(padded); 274 275 // XOR Whitening for final block 276 const mask = engine.rng.nextBytes(5); 277 const maskedChunk = Buffer.alloc(5); 278 for(let i=0; i<5; i++) maskedChunk[i] = padded[i] ^ mask[i]; 279 280 const enc = engine._encodeBase1024(maskedChunk); 281 engine.hmac.update(enc); 282 this.push(engine._wrapOutput(enc)); 283 } 284 285 const digest = engine.hmac.digest(); 286 let footerStr = ''; 287 for (const byte of digest) { 288 const hex = byte.toString(16).padStart(2, '0'); 289 for (const char of hex) { 290 const val = parseInt(char, 16); 291 footerStr += HEADER_ALPHABET[val]; 292 } 293 } 294 this.push(Buffer.from('\n' + footerStr)); 295 callback(); 296 } 297 }); 298 } 299 300 _wrapOutput(bufferChunk) { 301 this.lineLength += bufferChunk.length; 302 if (this.lineLength > 300) { 303 this.lineLength = 0; 304 return Buffer.concat([bufferChunk, Buffer.from('\n')]); 305 } 306 return bufferChunk; 307 } 308 309 _encodeBase1024(buffer5) { 310 let val = 0n; 311 for (let i = 0; i < 5; i++) { 312 val += BigInt(buffer5[i]) * (256n ** BigInt(i)); 313 } 314 315 let output = ''; 316 for (let i = 0; i < 4; i++) { 317 const idx = Number(val % 1024n); 318 val = val / 1024n; 319 output += this.dataAlphabet[idx]; 320 } 321 return Buffer.from(output); 322 } 323 324 _flushDataBuffer(buf) { 325 if (buf.length === 0) return Buffer.alloc(0); 326 const padded = Buffer.alloc(5); 327 buf.copy(padded); 328 329 // XOR Whitening 330 const mask = this.rng.nextBytes(5); 331 const maskedChunk = Buffer.alloc(5); 332 for(let i=0; i<5; i++) maskedChunk[i] = padded[i] ^ mask[i]; 333 334 const enc = this._encodeBase1024(maskedChunk); 335 this.hmac.update(enc); 336 return this._wrapOutput(enc); 337 } 338 339 // --- STREAMING DECRYPTION --- 340 341 getDecryptStream() { 342 if (!this.isReady) throw new Error("Engine not initialized"); 343 const engine = this; 344 const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' }); 345 346 const FOOTER_LEN = 64; 347 let emojiBuffer = []; 348 349 return new Transform({ 350 transform(chunk, encoding, callback) { 351 const str = chunk.toString('utf8'); 352 const segments = [...segmenter.segment(str)]; 353 354 for (const { segment } of segments) { 355 if (segment.match(/\s/)) continue; 356 357 const atoms = [...segment]; 358 359 for (const atom of atoms) { 360 emojiBuffer.push(atom); 361 362 if (emojiBuffer.length > FOOTER_LEN) { 363 const emo = emojiBuffer.shift(); 364 engine._processDecryptToken(emo, this); 365 engine.hmac.update(Buffer.from(emo)); 366 } 367 } 368 } 369 callback(); 370 }, 371 372 flush(callback) { 373 if (emojiBuffer.length !== FOOTER_LEN) { 374 this.emit('error', new Error("File corrupted or truncated (No Footer)")); 375 return; 376 } 377 378 const footerStr = emojiBuffer.join(''); 379 const calcDigest = engine.hmac.digest('hex'); 380 381 let footerHex = ''; 382 try { 383 for (const char of footerStr) { 384 const idx = HEADER_ALPHABET.indexOf(char); 385 if (idx === -1) throw new Error(); 386 footerHex += idx.toString(16); 387 } 388 } catch (e) { 389 this.emit('error', new Error("Invalid Integrity Seal")); 390 return; 391 } 392 393 const footerBuffer = Buffer.from(footerHex, 'hex'); 394 const calcBuffer = Buffer.from(calcDigest, 'hex'); 395 396 if (footerBuffer.length !== calcBuffer.length || !crypto.timingSafeEqual(footerBuffer, calcBuffer)) { 397 this.emit('error', new Error("FILE_TAMPERED")); 398 return; 399 } 400 callback(); 401 } 402 }); 403 } 404 405 decodeDataBuf = []; 406 407 _processDecryptToken(emo, stream) { 408 if (this.keywordReverseMap.has(emo)) { 409 const currentR = Number(this.rng.next() % BigInt(this.keywordEmojis.length)); 410 const emoIdx = this.keywordEmojis.indexOf(emo); 411 412 let baseIdx = (emoIdx - currentR) % this.keywordEmojis.length; 413 if (baseIdx < 0) baseIdx += this.keywordEmojis.length; 414 415 const originalEmo = this.keywordEmojis[baseIdx]; 416 const keyword = this.keywordReverseMap.get(originalEmo); 417 418 if (this.decodeDataBuf.length > 0) { 419 this.decodeDataBuf = []; 420 } 421 422 stream.push(keyword); 423 424 } else if (this.dataReverseMap.has(emo)) { 425 this.decodeDataBuf.push(this.dataReverseMap.get(emo)); 426 if (this.decodeDataBuf.length === 4) { 427 // We have a full encoded chunk (4 emojis) 428 const maskedChunk = this._decodeBase1024(this.decodeDataBuf); 429 this.decodeDataBuf = []; 430 431 // Get mask from RNG (Synchronized with encryption) 432 const mask = this.rng.nextBytes(5); 433 const chunk = Buffer.alloc(5); 434 435 // XOR Back to get plaintext 436 for(let i=0; i<5; i++) chunk[i] = maskedChunk[i] ^ mask[i]; 437 438 const cleanChunk = chunk.filter(b => b !== 0x00); 439 stream.push(cleanChunk); 440 } 441 } 442 } 443 444 _decodeBase1024(indices) { 445 let val = 0n; 446 for (let i = 3; i >= 0; i--) { 447 val = (val * 1024n) + BigInt(indices[i]); 448 } 449 450 const buf = Buffer.alloc(5); 451 for (let i = 0; i < 5; i++) { 452 buf[i] = Number(val % 256n); 453 val = val / 256n; 454 } 455 return buf; 456 } 457 }