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