mojic 1.2.5 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mojic.js +4 -2
- package/lib/CipherEngine.js +99 -52
- package/package.json +2 -1
package/bin/mojic.js
CHANGED
|
@@ -9,10 +9,12 @@ import { Transform } from 'stream';
|
|
|
9
9
|
import { StringDecoder } from 'string_decoder';
|
|
10
10
|
import { CipherEngine } from '../lib/CipherEngine.js';
|
|
11
11
|
|
|
12
|
+
const VERSION = '2.1.1';
|
|
13
|
+
|
|
12
14
|
program
|
|
13
15
|
.name('mojic')
|
|
14
16
|
.description('Obfuscate C source code into emojis')
|
|
15
|
-
.version(
|
|
17
|
+
.version(VERSION)
|
|
16
18
|
.addHelpCommand('help [command]', 'Display help for command')
|
|
17
19
|
.showHelpAfterError();
|
|
18
20
|
|
|
@@ -119,7 +121,7 @@ program
|
|
|
119
121
|
throw new Error(`'${targetPath}' is a directory. Use -r to process recursively.`);
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
console.log(chalk.blue(
|
|
124
|
+
console.log(chalk.blue(`Initiating Mojic Encryption v${VERSION}...`));
|
|
123
125
|
if (options.flat) console.log(chalk.yellow(' -> Structural Flattening Enabled'));
|
|
124
126
|
|
|
125
127
|
const password = await promptPassword('Create password for file(s):');
|
package/lib/CipherEngine.js
CHANGED
|
@@ -3,12 +3,15 @@ import { Transform } from 'stream';
|
|
|
3
3
|
import { StringDecoder } from 'string_decoder';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* MOJIC
|
|
7
|
-
* "Operation
|
|
6
|
+
* MOJIC v2.1.1 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
|
|
8
12
|
* * Fixes:
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* - Tokenization: Handles combined graphemes (skin tones/modifiers) during decryption
|
|
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.
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
// --- EMOJI UNIVERSE GENERATION ---
|
|
@@ -33,7 +36,7 @@ const generateUniverse = () => {
|
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
|
-
return universe;
|
|
39
|
+
return universe;
|
|
37
40
|
};
|
|
38
41
|
|
|
39
42
|
const RAW_UNIVERSE = generateUniverse();
|
|
@@ -50,41 +53,50 @@ const C_KEYWORDS = [
|
|
|
50
53
|
'include', 'define', 'main', 'printf', 'NULL', '#include', '#define'
|
|
51
54
|
];
|
|
52
55
|
|
|
53
|
-
// --- PRNG:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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();
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.
|
|
71
|
-
|
|
72
|
-
this.s[0] ^= this.s[3];
|
|
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
|
+
}
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
this.
|
|
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
|
+
}
|
|
76
84
|
|
|
77
|
-
|
|
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;
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
nextFloat() {
|
|
95
|
+
// Convert uint64 to double [0, 1)
|
|
96
|
+
// We take upper 53 bits for standard double precision
|
|
81
97
|
const val = Number(this.next() >> 11n);
|
|
82
98
|
return val * (2 ** -53);
|
|
83
99
|
}
|
|
84
|
-
|
|
85
|
-
rotl(x, k) {
|
|
86
|
-
return (x << k) | (x >> (64n - k));
|
|
87
|
-
}
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
export class CipherEngine {
|
|
@@ -96,24 +108,31 @@ export class CipherEngine {
|
|
|
96
108
|
this.dataReverseMap = new Map();
|
|
97
109
|
this.isReady = false;
|
|
98
110
|
this.hmac = null;
|
|
99
|
-
this.lineLength = 0;
|
|
111
|
+
this.lineLength = 0;
|
|
100
112
|
}
|
|
101
113
|
|
|
102
114
|
async init(existingSaltHex = null, expectedAuthCheck = null) {
|
|
103
115
|
this.salt = existingSaltHex
|
|
104
116
|
? Buffer.from(existingSaltHex, 'hex')
|
|
105
|
-
: crypto.randomBytes(
|
|
117
|
+
: crypto.randomBytes(32); // Increased salt size to 32 bytes
|
|
106
118
|
|
|
119
|
+
// SECURITY UPGRADE: Scrypt instead of PBKDF2
|
|
120
|
+
// N=16384, r=8, p=1 are standard secure defaults
|
|
107
121
|
const derivedKey = await new Promise((resolve, reject) => {
|
|
108
|
-
crypto.
|
|
122
|
+
crypto.scrypt(this.password, this.salt, 80, { N: 16384, r: 8, p: 1 }, (err, key) => {
|
|
109
123
|
if (err) reject(err); else resolve(key);
|
|
110
124
|
});
|
|
111
125
|
});
|
|
112
126
|
|
|
113
|
-
|
|
114
|
-
|
|
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);
|
|
115
135
|
|
|
116
|
-
// Check password correctness immediately if Auth Check is provided
|
|
117
136
|
if (expectedAuthCheck) {
|
|
118
137
|
const calculatedAuthCheck = this.authKey.subarray(0, 4).toString('hex');
|
|
119
138
|
if (calculatedAuthCheck !== expectedAuthCheck) {
|
|
@@ -121,7 +140,8 @@ export class CipherEngine {
|
|
|
121
140
|
}
|
|
122
141
|
}
|
|
123
142
|
|
|
124
|
-
|
|
143
|
+
// Initialize CSPRNG
|
|
144
|
+
this.rng = new AesCounterRNG(rngKey, rngIv);
|
|
125
145
|
|
|
126
146
|
const shuffled = this._shuffleArray([...RAW_UNIVERSE]);
|
|
127
147
|
|
|
@@ -146,6 +166,7 @@ export class CipherEngine {
|
|
|
146
166
|
}
|
|
147
167
|
|
|
148
168
|
_shuffleArray(array) {
|
|
169
|
+
// Fisher-Yates shuffle using CSPRNG
|
|
149
170
|
for (let i = array.length - 1; i > 0; i--) {
|
|
150
171
|
const j = Math.floor(this.rng.nextFloat() * (i + 1));
|
|
151
172
|
[array[i], array[j]] = [array[j], array[i]];
|
|
@@ -176,9 +197,16 @@ export class CipherEngine {
|
|
|
176
197
|
hexString += index.toString(16);
|
|
177
198
|
}
|
|
178
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
|
+
|
|
179
207
|
return {
|
|
180
|
-
saltHex: hexString.substring(0,
|
|
181
|
-
authCheckHex: hexString.
|
|
208
|
+
saltHex: hexString.substring(0, authStart),
|
|
209
|
+
authCheckHex: hexString.substring(authStart)
|
|
182
210
|
};
|
|
183
211
|
}
|
|
184
212
|
|
|
@@ -193,11 +221,9 @@ export class CipherEngine {
|
|
|
193
221
|
transform(chunk, encoding, callback) {
|
|
194
222
|
const str = chunk.toString('utf8');
|
|
195
223
|
|
|
196
|
-
// Separate alpha keywords (int, void) from symbols (#include)
|
|
197
224
|
const alphaKeywords = C_KEYWORDS.filter(k => /^\w+$/.test(k)).sort((a,b)=>b.length-a.length).join('|');
|
|
198
225
|
const symKeywords = C_KEYWORDS.filter(k => !/^\w+$/.test(k)).map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
199
226
|
|
|
200
|
-
// Match: \b(int|void)\b OR (#include)
|
|
201
227
|
const regex = new RegExp(`(\\b(?:${alphaKeywords})\\b|(?:${symKeywords}))`, 'g');
|
|
202
228
|
|
|
203
229
|
const parts = str.split(regex);
|
|
@@ -227,7 +253,12 @@ export class CipherEngine {
|
|
|
227
253
|
const chunk = buffer.subarray(0, 5);
|
|
228
254
|
buffer = buffer.subarray(5);
|
|
229
255
|
|
|
230
|
-
|
|
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);
|
|
231
262
|
engine.hmac.update(enc);
|
|
232
263
|
this.push(engine._wrapOutput(enc));
|
|
233
264
|
}
|
|
@@ -240,7 +271,13 @@ export class CipherEngine {
|
|
|
240
271
|
if (buffer.length > 0) {
|
|
241
272
|
const padded = Buffer.alloc(5);
|
|
242
273
|
buffer.copy(padded);
|
|
243
|
-
|
|
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);
|
|
244
281
|
engine.hmac.update(enc);
|
|
245
282
|
this.push(engine._wrapOutput(enc));
|
|
246
283
|
}
|
|
@@ -288,7 +325,13 @@ export class CipherEngine {
|
|
|
288
325
|
if (buf.length === 0) return Buffer.alloc(0);
|
|
289
326
|
const padded = Buffer.alloc(5);
|
|
290
327
|
buf.copy(padded);
|
|
291
|
-
|
|
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);
|
|
292
335
|
this.hmac.update(enc);
|
|
293
336
|
return this._wrapOutput(enc);
|
|
294
337
|
}
|
|
@@ -309,11 +352,8 @@ export class CipherEngine {
|
|
|
309
352
|
const segments = [...segmenter.segment(str)];
|
|
310
353
|
|
|
311
354
|
for (const { segment } of segments) {
|
|
312
|
-
if (segment.match(/\s/)) continue;
|
|
355
|
+
if (segment.match(/\s/)) continue;
|
|
313
356
|
|
|
314
|
-
// FIX: Some emojis in the universe (like skin tones 0x1F3FB) are modifiers.
|
|
315
|
-
// If they appear next to another emoji, Intl.Segmenter merges them.
|
|
316
|
-
// We must split them back into atomic code points to preserve token count.
|
|
317
357
|
const atoms = [...segment];
|
|
318
358
|
|
|
319
359
|
for (const atom of atoms) {
|
|
@@ -322,7 +362,6 @@ export class CipherEngine {
|
|
|
322
362
|
if (emojiBuffer.length > FOOTER_LEN) {
|
|
323
363
|
const emo = emojiBuffer.shift();
|
|
324
364
|
engine._processDecryptToken(emo, this);
|
|
325
|
-
// Feed actual atomic tokens to HMAC to match encryption
|
|
326
365
|
engine.hmac.update(Buffer.from(emo));
|
|
327
366
|
}
|
|
328
367
|
}
|
|
@@ -373,7 +412,6 @@ export class CipherEngine {
|
|
|
373
412
|
const originalEmo = this.keywordEmojis[baseIdx];
|
|
374
413
|
const keyword = this.keywordReverseMap.get(originalEmo);
|
|
375
414
|
|
|
376
|
-
// Safety: This buffer should be empty if the stream is synced.
|
|
377
415
|
if (this.decodeDataBuf.length > 0) {
|
|
378
416
|
this.decodeDataBuf = [];
|
|
379
417
|
}
|
|
@@ -383,8 +421,17 @@ export class CipherEngine {
|
|
|
383
421
|
} else if (this.dataReverseMap.has(emo)) {
|
|
384
422
|
this.decodeDataBuf.push(this.dataReverseMap.get(emo));
|
|
385
423
|
if (this.decodeDataBuf.length === 4) {
|
|
386
|
-
|
|
424
|
+
// We have a full encoded chunk (4 emojis)
|
|
425
|
+
const maskedChunk = this._decodeBase1024(this.decodeDataBuf);
|
|
387
426
|
this.decodeDataBuf = [];
|
|
427
|
+
|
|
428
|
+
// Get mask from RNG (Synchronized with encryption)
|
|
429
|
+
const mask = this.rng.nextBytes(5);
|
|
430
|
+
const chunk = Buffer.alloc(5);
|
|
431
|
+
|
|
432
|
+
// XOR Back to get plaintext
|
|
433
|
+
for(let i=0; i<5; i++) chunk[i] = maskedChunk[i] ^ mask[i];
|
|
434
|
+
|
|
388
435
|
const cleanChunk = chunk.filter(b => b !== 0x00);
|
|
389
436
|
stream.push(cleanChunk);
|
|
390
437
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mojic",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Obfuscate C source code into encrypted, password-seeded emoji streams.",
|
|
5
5
|
"main": "bin/mojic.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@notamitgamer/mojic": "^1.2.5",
|
|
23
24
|
"chalk": "^5.3.0",
|
|
24
25
|
"commander": "^11.1.0",
|
|
25
26
|
"inquirer": "^9.2.12"
|