mojic 1.0.3 → 1.1.0
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.
Potentially problematic release.
This version of mojic might be problematic. Click here for more details.
- package/CONTRIBUTING.md +43 -11
- package/README.md +53 -23
- package/SECURITY.md +2 -2
- package/bin/mojic.js +96 -62
- package/lib/CipherEngine.js +296 -140
- package/package.json +23 -5
package/CONTRIBUTING.md
CHANGED
|
@@ -1,23 +1,56 @@
|
|
|
1
1
|
# Contributing to Mojic
|
|
2
2
|
|
|
3
|
-
First off, thanks for taking the time to contribute!
|
|
3
|
+
First off, thanks for taking the time to contribute!
|
|
4
4
|
|
|
5
5
|
The following is a set of guidelines for contributing to Mojic. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Development Workflow
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
We use the standard GitHub Pull Request workflow.
|
|
10
|
+
|
|
11
|
+
1. **Fork** the repository on GitHub:
|
|
12
|
+
[https://github.com/notamitgamer/mojic](https://github.com/notamitgamer/mojic)
|
|
13
|
+
|
|
14
|
+
2. **Clone** your fork locally:
|
|
15
|
+
```bash
|
|
16
|
+
git clone [https://github.com/YOUR_USERNAME/mojic.git](https://github.com/YOUR_USERNAME/mojic.git)
|
|
17
|
+
cd mojic
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
3. **Create a Branch** for your feature or bugfix:
|
|
21
|
+
```bash
|
|
22
|
+
git checkout -b feature/amazing-feature
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
4. **Install Dependencies**:
|
|
26
|
+
```bash
|
|
27
|
+
npm install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
5. **Test your changes**:
|
|
31
|
+
You can run the CLI locally using `npm start` or linking the package.
|
|
32
|
+
```bash
|
|
33
|
+
# Run directly
|
|
34
|
+
npm start -- encode test.c
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
6. **Commit** your changes (see the style guide below).
|
|
10
38
|
|
|
11
|
-
|
|
39
|
+
7. **Push** to your fork:
|
|
40
|
+
```bash
|
|
41
|
+
git push origin feature/amazing-feature
|
|
42
|
+
```
|
|
12
43
|
|
|
44
|
+
8. **Open a Pull Request** on the main repository (`notamitgamer/mojic`).
|
|
45
|
+
|
|
46
|
+
## How Can I Contribute?
|
|
47
|
+
|
|
48
|
+
### Reporting Bugs
|
|
13
49
|
* **Use a clear and descriptive title** for the issue to identify the problem.
|
|
14
50
|
* **Describe the exact steps which reproduce the problem** in as many details as possible.
|
|
15
51
|
* **Provide specific examples** to demonstrate the steps.
|
|
16
52
|
|
|
17
53
|
### Suggesting Enhancements
|
|
18
|
-
|
|
19
|
-
This section guides you through submitting an enhancement suggestion for Mojic, including completely new features and minor improvements to existing functionality.
|
|
20
|
-
|
|
21
54
|
* **Use a clear and descriptive title** for the issue to identify the suggestion.
|
|
22
55
|
* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
|
|
23
56
|
* **Explain why this enhancement would be useful** to most Mojic users.
|
|
@@ -25,14 +58,13 @@ This section guides you through submitting an enhancement suggestion for Mojic,
|
|
|
25
58
|
## Styleguides
|
|
26
59
|
|
|
27
60
|
### Git Commit Messages
|
|
28
|
-
|
|
29
61
|
* Use the present tense ("Add feature" not "Added feature")
|
|
30
62
|
* Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
|
|
31
63
|
* Limit the first line to 72 characters or less
|
|
32
64
|
|
|
33
65
|
### Mojic Code Style
|
|
34
|
-
|
|
35
|
-
* **Cipher Logic:** Changes to `CipherEngine.js` must ensure backward compatibility with the header format.
|
|
66
|
+
* **Cipher Logic:** Changes to `CipherEngine.js` must ensure backward compatibility with the header format if possible.
|
|
36
67
|
* **Streams:** Always use `StringDecoder` when handling text streams to prevent multi-byte emoji corruption.
|
|
68
|
+
* **Linting:** Ensure your code is clean and readable.
|
|
37
69
|
|
|
38
|
-
Happy Hacking!
|
|
70
|
+
Happy Hacking!
|
package/README.md
CHANGED
|
@@ -1,27 +1,36 @@
|
|
|
1
|
-
# Mojic
|
|
1
|
+
# Mojic v1.1.0
|
|
2
2
|
|
|
3
|
-
> **Obfuscate C source code into a randomized stream of emojis
|
|
3
|
+
> **Operation Polymorphic Chaos: Obfuscate C source code into a randomized, password-seeded stream of emojis.**
|
|
4
4
|
|
|
5
|
-
**Mojic** (Magic + Emoji + Logic) is a CLI tool
|
|
5
|
+
**Mojic** (Magic + Emoji + Logic) is a sophisticated CLI tool designed to transform readable C code into an unrecognizable chaotic stream of emojis. Unlike simple substitution ciphers, Mojic uses your password to seed a cryptographically strong Pseudo-Random Number Generator (PRNG), creating a unique "Emoji Universe" and rolling cipher for every single session.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Key Features
|
|
8
8
|
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* ** Xoshiro256** PRNG:** Uses a high-quality 256-bit state PRNG (seeded via PBKDF2-SHA512) to handle shuffling and polymorphism.
|
|
10
|
+
* ** Polymorphic Keywords:** Common C keywords (`int`, `void`, `return`) are mapped to emojis that *change* every time they appear based on the PRNG state. Frequency analysis is impossible.
|
|
11
|
+
* ** Base-1024 Compression:** Non-keyword code is compressed using a custom Base-1024 scheme (5 bytes → 4 emojis), keeping file size manageable.
|
|
12
|
+
* ** Integrity Sealed:** Every file ends with an HMAC-SHA256 signature. Any tampering with the emoji stream results in an immediate `FILE_TAMPERED` error.
|
|
13
|
+
* ** Moon Header Protocol:** Metadata (Salt + Auth Check) is encoded using a specific alphabet of Moon and Clock phases (`🌑🌒🕐`), allowing instant password verification before decryption starts.
|
|
14
|
+
* ** Stream Architecture:** Built on Node.js `Transform` streams to handle large files efficiently with minimal memory footprint.
|
|
14
15
|
|
|
15
|
-
##
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Since Mojic is available on npm, you can install it globally with a single command:
|
|
16
19
|
|
|
17
20
|
```bash
|
|
18
21
|
npm install -g mojic
|
|
19
22
|
```
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
Or run it directly using `npx` without installing:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx mojic encode main.c
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
22
31
|
|
|
23
32
|
### 1. Encrypting Code (`encode`)
|
|
24
|
-
|
|
33
|
+
Transforms a `.c` file into a `.mojic` file.
|
|
25
34
|
|
|
26
35
|
```bash
|
|
27
36
|
# Encrypt a single file
|
|
@@ -29,6 +38,9 @@ mojic encode main.c
|
|
|
29
38
|
|
|
30
39
|
# Encrypt an entire directory recursively
|
|
31
40
|
mojic encode ./src -r
|
|
41
|
+
|
|
42
|
+
# Flatten/Minify code structure before encryption (Removes newlines/indentation)
|
|
43
|
+
mojic encode main.c --flat
|
|
32
44
|
```
|
|
33
45
|
*You will be prompted to create a password. This password is required to decrypt.*
|
|
34
46
|
|
|
@@ -44,24 +56,42 @@ mojic decode ./src -r
|
|
|
44
56
|
```
|
|
45
57
|
|
|
46
58
|
### 3. Security & Rotation Tools (`srt`)
|
|
47
|
-
Manage encrypted files without revealing their contents.
|
|
59
|
+
Manage encrypted files without ever revealing their plaintext contents.
|
|
48
60
|
|
|
49
61
|
```bash
|
|
50
|
-
#
|
|
62
|
+
# Rotate Password: Changes the password of an encrypted file
|
|
51
63
|
mojic srt --pass secret.mojic
|
|
52
64
|
|
|
53
|
-
# Re-
|
|
54
|
-
# (Useful
|
|
65
|
+
# Re-Encrypt: Re-shuffles the entropy (New Salt) with the SAME password
|
|
66
|
+
# (Useful to change the visual emoji pattern without changing the password)
|
|
55
67
|
mojic srt --re secret.mojic
|
|
56
68
|
```
|
|
57
69
|
|
|
58
|
-
##
|
|
70
|
+
## Under the Hood (Algorithm)
|
|
71
|
+
|
|
72
|
+
Mojic v1.1.0 implements a custom crypto-system dubbed **"Operation Polymorphic Chaos"**.
|
|
73
|
+
|
|
74
|
+
1. **Derivation Phase:**
|
|
75
|
+
* **Input:** User Password + 16-byte Random Salt.
|
|
76
|
+
* **KDF:** `PBKDF2-SHA512` (100,000 iterations).
|
|
77
|
+
* **Output:** 64 bytes (32 bytes for PRNG Seed, 32 bytes for HMAC Auth Key).
|
|
78
|
+
|
|
79
|
+
2. **The Emoji Universe:**
|
|
80
|
+
* The engine generates a universe of ~1,100 valid emojis (Emoticons, Transport, Symbols).
|
|
81
|
+
* This universe is **shuffled** using the `Xoshiro256**` PRNG initialized with the derived seed.
|
|
82
|
+
|
|
83
|
+
3. **Polymorphic Encryption:**
|
|
84
|
+
* **C Keywords:** The engine detects C keywords (e.g., `while`). It assigns them a "Base Emoji" from the shuffled universe.
|
|
85
|
+
* **The Twist:** It doesn't just print the Base Emoji. It calculates a random offset using the PRNG to pick a *different* emoji that maps back to the keyword. This means `int` might look like `🚀` on line 1 and `🌮` on line 5.
|
|
86
|
+
|
|
87
|
+
4. **Base-1024 Encoding:**
|
|
88
|
+
* Non-keyword data is buffered into 5-byte chunks.
|
|
89
|
+
* These chunks are treated as a single large integer and converted into 4 base-1024 digits (mapped to emojis), effectively compressing the stream density.
|
|
59
90
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
4. **The Header:** The Salt and Auth Hash are written to the top of the file using a fixed "Header Alphabet" (🌑🌒🌓🌔...) so the tool can verify your password before attempting decryption.
|
|
91
|
+
5. **The Header:**
|
|
92
|
+
* The Salt and a 4-byte Auth Check are written to the file header using the **Moon/Clock Alphabet** (`🌑🌒🌓🌔...`).
|
|
93
|
+
* **Benefit:** This allows `mojic` to tell you "Incorrect Password" instantly, rather than churning out garbage data first.
|
|
64
94
|
|
|
65
|
-
##
|
|
95
|
+
## License
|
|
66
96
|
|
|
67
|
-
This project is licensed under the Apache License 2.0
|
|
97
|
+
This project is licensed under the Apache License 2.0.
|
package/SECURITY.md
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
We take security seriously.
|
|
6
6
|
|
|
7
|
-
If you discover a security vulnerability within Mojic (e.g., in the `CipherEngine` logic or the PRNG implementation), please send an e-mail to **amitdutta4255@gmail.com**. All security vulnerabilities will be promptly addressed.
|
|
7
|
+
If you discover a security vulnerability within Mojic (e.g., in the `CipherEngine` logic or the PRNG implementation), please send an e-mail to **amitdutta4255@gmail.com** or **mail@amit.is-a.dev**. All security vulnerabilities will be promptly addressed.
|
|
8
8
|
|
|
9
9
|
**Please do not open public issues for security vulnerabilities.**
|
|
10
10
|
|
|
11
11
|
### Scope
|
|
12
|
-
* **Supported:** Issues regarding key derivation, salt collisions, or stream buffer overflows.
|
|
12
|
+
* **Supported:** Issues regarding key derivation, salt collisions, HMAC integrity failures, or stream buffer overflows.
|
|
13
13
|
* **Not Supported:** Brute-forcing weak passwords (users are responsible for password strength).
|
package/bin/mojic.js
CHANGED
|
@@ -12,7 +12,7 @@ import { CipherEngine } from '../lib/CipherEngine.js';
|
|
|
12
12
|
program
|
|
13
13
|
.name('mojic')
|
|
14
14
|
.description('Obfuscate C source code into emojis')
|
|
15
|
-
.version('1.0
|
|
15
|
+
.version('1.1.0')
|
|
16
16
|
.addHelpCommand('help [command]', 'Display help for command')
|
|
17
17
|
.showHelpAfterError();
|
|
18
18
|
|
|
@@ -78,6 +78,19 @@ const createHeaderSkipper = () => {
|
|
|
78
78
|
});
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
// --- FLATTENING (Minifier) ---
|
|
82
|
+
const createMinifier = () => {
|
|
83
|
+
return new Transform({
|
|
84
|
+
transform(chunk, encoding, cb) {
|
|
85
|
+
let str = chunk.toString();
|
|
86
|
+
str = str.replace(/\r?\n|\r/g, ' ');
|
|
87
|
+
str = str.replace(/\s+/g, ' ');
|
|
88
|
+
this.push(str);
|
|
89
|
+
cb();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
81
94
|
// --- Recursive Logic ---
|
|
82
95
|
|
|
83
96
|
async function traverseDirectory(currentPath, extension, callback) {
|
|
@@ -99,18 +112,20 @@ program
|
|
|
99
112
|
.command('encode')
|
|
100
113
|
.argument('<path>', 'File or Directory to encode')
|
|
101
114
|
.option('-r, --recursive', 'Recursively encrypt all .c files in directory')
|
|
115
|
+
.option('-f, --flat', 'Flatten structure (strip whitespace/newlines) before encrypting')
|
|
102
116
|
.description('Encrypt a file or directory into emojis')
|
|
103
117
|
.action(async (targetPath, options) => {
|
|
104
118
|
try {
|
|
105
119
|
if (!fs.existsSync(targetPath)) throw new Error('Path not found');
|
|
106
120
|
const stats = fs.statSync(targetPath);
|
|
107
121
|
|
|
108
|
-
// Validation
|
|
109
122
|
if (stats.isDirectory() && !options.recursive) {
|
|
110
123
|
throw new Error(`'${targetPath}' is a directory. Use -r to process recursively.`);
|
|
111
124
|
}
|
|
112
125
|
|
|
113
|
-
console.log(chalk.blue('
|
|
126
|
+
console.log(chalk.blue('Initiating Mojic Encryption v1.1...'));
|
|
127
|
+
if (options.flat) console.log(chalk.yellow(' -> Structural Flattening Enabled'));
|
|
128
|
+
|
|
114
129
|
const password = await promptPassword('Create password for file(s):');
|
|
115
130
|
|
|
116
131
|
const processFile = async (filePath) => {
|
|
@@ -118,7 +133,7 @@ program
|
|
|
118
133
|
console.log(chalk.dim(` Processing: ${path.basename(filePath)} -> ${path.basename(outputName)}`));
|
|
119
134
|
|
|
120
135
|
const engine = new CipherEngine(password);
|
|
121
|
-
await engine.init();
|
|
136
|
+
await engine.init();
|
|
122
137
|
|
|
123
138
|
const readStream = fs.createReadStream(filePath);
|
|
124
139
|
const writeStream = fs.createWriteStream(outputName);
|
|
@@ -126,7 +141,13 @@ program
|
|
|
126
141
|
writeStream.write(engine._encodeHeader());
|
|
127
142
|
|
|
128
143
|
await new Promise((resolve, reject) => {
|
|
129
|
-
readStream
|
|
144
|
+
let pipeline = readStream;
|
|
145
|
+
|
|
146
|
+
if (options.flat) {
|
|
147
|
+
pipeline = pipeline.pipe(createMinifier());
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
pipeline
|
|
130
151
|
.pipe(engine.getEncryptStream())
|
|
131
152
|
.pipe(writeStream)
|
|
132
153
|
.on('finish', resolve)
|
|
@@ -135,12 +156,12 @@ program
|
|
|
135
156
|
};
|
|
136
157
|
|
|
137
158
|
if (stats.isDirectory()) {
|
|
138
|
-
console.log(chalk.blue(
|
|
159
|
+
console.log(chalk.blue(`Scanning directory: ${targetPath}`));
|
|
139
160
|
await traverseDirectory(targetPath, '.c', processFile);
|
|
140
|
-
console.log(chalk.green('
|
|
161
|
+
console.log(chalk.green('Batch encryption complete.'));
|
|
141
162
|
} else {
|
|
142
163
|
await processFile(targetPath);
|
|
143
|
-
console.log(chalk.green(
|
|
164
|
+
console.log(chalk.green(`Encrypted.`));
|
|
144
165
|
}
|
|
145
166
|
|
|
146
167
|
} catch (err) {
|
|
@@ -162,8 +183,8 @@ program
|
|
|
162
183
|
throw new Error(`'${targetPath}' is a directory. Use -r to process recursively.`);
|
|
163
184
|
}
|
|
164
185
|
|
|
165
|
-
console.log(chalk.blue('
|
|
166
|
-
const password = await promptPassword('Enter password:');
|
|
186
|
+
console.log(chalk.blue('Initiating Decryption...'));
|
|
187
|
+
const password = await promptPassword('Enter password:');
|
|
167
188
|
|
|
168
189
|
const processFile = async (filePath) => {
|
|
169
190
|
try {
|
|
@@ -174,36 +195,61 @@ program
|
|
|
174
195
|
const metadata = CipherEngine.decodeHeader(headerStr);
|
|
175
196
|
|
|
176
197
|
const engine = new CipherEngine(password);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
console.log(chalk.red(` ❌ Skipped ${path.basename(filePath)}: Incorrect Password`));
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
198
|
+
|
|
199
|
+
// Pass the Auth Check Hex (if available in new format)
|
|
200
|
+
await engine.init(metadata.saltHex, metadata.authCheckHex);
|
|
183
201
|
|
|
184
202
|
const readStream = fs.createReadStream(filePath);
|
|
185
203
|
const writeStream = fs.createWriteStream(outputName);
|
|
186
204
|
|
|
187
205
|
await new Promise((resolve, reject) => {
|
|
206
|
+
const decryptStream = engine.getDecryptStream();
|
|
207
|
+
|
|
208
|
+
decryptStream.on('error', (err) => {
|
|
209
|
+
reject(err);
|
|
210
|
+
});
|
|
211
|
+
|
|
188
212
|
readStream
|
|
189
213
|
.pipe(createHeaderSkipper())
|
|
190
|
-
.pipe(
|
|
214
|
+
.pipe(decryptStream)
|
|
191
215
|
.pipe(writeStream)
|
|
192
216
|
.on('finish', resolve)
|
|
193
217
|
.on('error', reject);
|
|
194
218
|
});
|
|
219
|
+
|
|
220
|
+
return true; // Success
|
|
195
221
|
} catch (e) {
|
|
196
|
-
|
|
222
|
+
const outputName = filePath.replace(/\.mojic$/, '') + '.restored.c';
|
|
223
|
+
|
|
224
|
+
// Cleanup output file on error
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
if (fs.existsSync(outputName)) {
|
|
227
|
+
try { fs.unlinkSync(outputName); } catch(ign){}
|
|
228
|
+
}
|
|
229
|
+
}, 100);
|
|
230
|
+
|
|
231
|
+
// Specific Error Messaging (No Emojis)
|
|
232
|
+
if (e.message === "WRONG_PASSWORD") {
|
|
233
|
+
console.log(chalk.red(` Error: Incorrect Password`));
|
|
234
|
+
} else if (e.message === "FILE_TAMPERED") {
|
|
235
|
+
console.log(chalk.red(` Error: File Tampered! Integrity check failed.`));
|
|
236
|
+
} else {
|
|
237
|
+
console.log(chalk.red(` Error: ${e.message}`));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return false; // Failure
|
|
197
241
|
}
|
|
198
242
|
};
|
|
199
243
|
|
|
200
244
|
if (stats.isDirectory()) {
|
|
201
|
-
console.log(chalk.blue(
|
|
245
|
+
console.log(chalk.blue(`Scanning directory: ${targetPath}`));
|
|
202
246
|
await traverseDirectory(targetPath, '.mojic', processFile);
|
|
203
|
-
console.log(chalk.green('
|
|
247
|
+
console.log(chalk.green('Batch decryption complete.'));
|
|
204
248
|
} else {
|
|
205
|
-
await processFile(targetPath);
|
|
206
|
-
|
|
249
|
+
const success = await processFile(targetPath);
|
|
250
|
+
if (success) {
|
|
251
|
+
console.log(chalk.green(`Restored.`));
|
|
252
|
+
}
|
|
207
253
|
}
|
|
208
254
|
|
|
209
255
|
} catch (err) {
|
|
@@ -211,32 +257,24 @@ program
|
|
|
211
257
|
}
|
|
212
258
|
});
|
|
213
259
|
|
|
214
|
-
// ---
|
|
260
|
+
// --- SRT ---
|
|
215
261
|
|
|
216
262
|
const rotatePassword = async (file) => {
|
|
217
263
|
try {
|
|
218
264
|
if (!fs.existsSync(file)) throw new Error('File not found');
|
|
219
|
-
console.log(chalk.yellow(
|
|
265
|
+
console.log(chalk.yellow(`Rotating Password for ${path.basename(file)}...`));
|
|
220
266
|
|
|
221
|
-
// 1. Authenticate Old
|
|
222
267
|
const headerStr = await getStreamHeader(file);
|
|
223
268
|
const metadata = CipherEngine.decodeHeader(headerStr);
|
|
224
269
|
const oldPass = await promptPassword('Enter CURRENT password:');
|
|
225
270
|
|
|
226
271
|
const oldEngine = new CipherEngine(oldPass);
|
|
227
|
-
await oldEngine.init(metadata.saltHex);
|
|
272
|
+
await oldEngine.init(metadata.saltHex, metadata.authCheckHex);
|
|
228
273
|
|
|
229
|
-
if (oldEngine.authHash.toString('hex') !== metadata.authHex) {
|
|
230
|
-
console.error(chalk.red('❌ Incorrect Current Password'));
|
|
231
|
-
process.exit(1);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// 2. Init New
|
|
235
274
|
const newPass = await promptPassword('Enter NEW password:');
|
|
236
275
|
const newEngine = new CipherEngine(newPass);
|
|
237
276
|
await newEngine.init();
|
|
238
277
|
|
|
239
|
-
// 3. Process
|
|
240
278
|
const tempFile = file + '.tmp';
|
|
241
279
|
const readStream = fs.createReadStream(file);
|
|
242
280
|
const writeStream = fs.createWriteStream(tempFile);
|
|
@@ -244,9 +282,13 @@ const rotatePassword = async (file) => {
|
|
|
244
282
|
writeStream.write(newEngine._encodeHeader());
|
|
245
283
|
|
|
246
284
|
await new Promise((resolve, reject) => {
|
|
285
|
+
const decryptStream = oldEngine.getDecryptStream();
|
|
286
|
+
|
|
287
|
+
decryptStream.on('error', (err) => reject(err));
|
|
288
|
+
|
|
247
289
|
readStream
|
|
248
290
|
.pipe(createHeaderSkipper())
|
|
249
|
-
.pipe(
|
|
291
|
+
.pipe(decryptStream)
|
|
250
292
|
.pipe(newEngine.getEncryptStream())
|
|
251
293
|
.pipe(writeStream)
|
|
252
294
|
.on('finish', resolve)
|
|
@@ -254,36 +296,33 @@ const rotatePassword = async (file) => {
|
|
|
254
296
|
});
|
|
255
297
|
|
|
256
298
|
fs.renameSync(tempFile, file);
|
|
257
|
-
console.log(chalk.green(
|
|
299
|
+
console.log(chalk.green(`Password updated.`));
|
|
258
300
|
|
|
259
301
|
} catch (err) {
|
|
260
|
-
|
|
302
|
+
if (fs.existsSync(file + '.tmp')) fs.unlinkSync(file + '.tmp');
|
|
303
|
+
if (err.message === "WRONG_PASSWORD") {
|
|
304
|
+
console.error(chalk.red('Error: Incorrect Current Password'));
|
|
305
|
+
} else {
|
|
306
|
+
console.error(chalk.red('Error:'), err.message);
|
|
307
|
+
}
|
|
261
308
|
}
|
|
262
309
|
};
|
|
263
310
|
|
|
264
311
|
const reEncrypt = async (file) => {
|
|
265
312
|
try {
|
|
266
313
|
if (!fs.existsSync(file)) throw new Error('File not found');
|
|
267
|
-
console.log(chalk.yellow(
|
|
314
|
+
console.log(chalk.yellow(`Re-shuffling Entropy for ${path.basename(file)}...`));
|
|
268
315
|
|
|
269
|
-
// 1. Authenticate
|
|
270
316
|
const headerStr = await getStreamHeader(file);
|
|
271
317
|
const metadata = CipherEngine.decodeHeader(headerStr);
|
|
272
318
|
const password = await promptPassword('Enter password:');
|
|
273
319
|
|
|
274
320
|
const oldEngine = new CipherEngine(password);
|
|
275
|
-
await oldEngine.init(metadata.saltHex);
|
|
321
|
+
await oldEngine.init(metadata.saltHex, metadata.authCheckHex);
|
|
276
322
|
|
|
277
|
-
if (oldEngine.authHash.toString('hex') !== metadata.authHex) {
|
|
278
|
-
console.error(chalk.red('❌ Incorrect Password'));
|
|
279
|
-
process.exit(1);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// 2. Init New
|
|
283
323
|
const newEngine = new CipherEngine(password);
|
|
284
324
|
await newEngine.init();
|
|
285
325
|
|
|
286
|
-
// 3. Process
|
|
287
326
|
const tempFile = file + '.tmp';
|
|
288
327
|
const readStream = fs.createReadStream(file);
|
|
289
328
|
const writeStream = fs.createWriteStream(tempFile);
|
|
@@ -291,9 +330,12 @@ const reEncrypt = async (file) => {
|
|
|
291
330
|
writeStream.write(newEngine._encodeHeader());
|
|
292
331
|
|
|
293
332
|
await new Promise((resolve, reject) => {
|
|
333
|
+
const decryptStream = oldEngine.getDecryptStream();
|
|
334
|
+
decryptStream.on('error', (err) => reject(err));
|
|
335
|
+
|
|
294
336
|
readStream
|
|
295
337
|
.pipe(createHeaderSkipper())
|
|
296
|
-
.pipe(
|
|
338
|
+
.pipe(decryptStream)
|
|
297
339
|
.pipe(newEngine.getEncryptStream())
|
|
298
340
|
.pipe(writeStream)
|
|
299
341
|
.on('finish', resolve)
|
|
@@ -301,10 +343,15 @@ const reEncrypt = async (file) => {
|
|
|
301
343
|
});
|
|
302
344
|
|
|
303
345
|
fs.renameSync(tempFile, file);
|
|
304
|
-
console.log(chalk.green(
|
|
346
|
+
console.log(chalk.green(`File re-encrypted.`));
|
|
305
347
|
|
|
306
348
|
} catch (err) {
|
|
307
|
-
|
|
349
|
+
if (fs.existsSync(file + '.tmp')) fs.unlinkSync(file + '.tmp');
|
|
350
|
+
if (err.message === "WRONG_PASSWORD") {
|
|
351
|
+
console.error(chalk.red('Error: Incorrect Password'));
|
|
352
|
+
} else {
|
|
353
|
+
console.error(chalk.red('Error:'), err.message);
|
|
354
|
+
}
|
|
308
355
|
}
|
|
309
356
|
};
|
|
310
357
|
|
|
@@ -313,11 +360,6 @@ program
|
|
|
313
360
|
.description('Security and Rotation Tools')
|
|
314
361
|
.option('--pass <file>', 'Update the password for an existing file')
|
|
315
362
|
.option('--re <file>', 'Re-encrypt with new random seed (Same password)')
|
|
316
|
-
.addHelpText('after', `
|
|
317
|
-
Examples:
|
|
318
|
-
$ mojic srt --pass secret.mojic # Change the password of an encrypted file
|
|
319
|
-
$ mojic srt --re secret.mojic # Re-scramble the emojis (new salt) with same password
|
|
320
|
-
`)
|
|
321
363
|
.action(async (options) => {
|
|
322
364
|
if (options.pass) {
|
|
323
365
|
await rotatePassword(options.pass);
|
|
@@ -329,12 +371,4 @@ Examples:
|
|
|
329
371
|
}
|
|
330
372
|
});
|
|
331
373
|
|
|
332
|
-
program.addHelpText('after', `
|
|
333
|
-
Usage Examples:
|
|
334
|
-
$ mojic encode test.c # Encrypt a single C file
|
|
335
|
-
$ mojic encode ./src -r # Recursively encrypt all .c files in ./src
|
|
336
|
-
$ mojic decode test.mojic # Decrypt a single file
|
|
337
|
-
$ mojic decode ./src -r # Recursively decrypt all .mojic files
|
|
338
|
-
`);
|
|
339
|
-
|
|
340
374
|
program.parse(process.argv);
|
package/lib/CipherEngine.js
CHANGED
|
@@ -3,109 +3,154 @@ import { Transform } from 'stream';
|
|
|
3
3
|
import { StringDecoder } from 'string_decoder';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* *
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
6
|
+
* MOJIC v1.1.0 CIPHER ENGINE
|
|
7
|
+
* "Operation Polymorphic Chaos"
|
|
8
|
+
* * Features:
|
|
9
|
+
* - Xoshiro256** PRNG (256-bit State)
|
|
10
|
+
* - Base-1024 Compression (5 bytes -> 4 emojis)
|
|
11
|
+
* - Polymorphic Keyword Encryption (Rolling Cipher)
|
|
12
|
+
* - HMAC-SHA256 Integrity Footer
|
|
13
|
+
* - Line Wrapping (New: Prevents lag in editors)
|
|
14
|
+
* - Instant Password Verification (New: Header Auth Check)
|
|
11
15
|
*/
|
|
12
16
|
|
|
17
|
+
// --- EMOJI UNIVERSE GENERATION ---
|
|
13
18
|
const HEADER_ALPHABET = ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗'];
|
|
14
19
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
20
|
+
const generateUniverse = () => {
|
|
21
|
+
const universe = [];
|
|
22
|
+
const ranges = [
|
|
23
|
+
[0x1F600, 0x1F64F], // Emoticons
|
|
24
|
+
[0x1F300, 0x1F5FF], // Misc Symbols
|
|
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; // Expect > 1100 chars
|
|
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
|
+
}
|
|
37
47
|
|
|
38
48
|
const C_KEYWORDS = [
|
|
39
49
|
'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do',
|
|
40
50
|
'double', 'else', 'enum', 'extern', 'float', 'for', 'goto', 'if',
|
|
41
51
|
'int', 'long', 'register', 'return', 'short', 'signed', 'sizeof', 'static',
|
|
42
52
|
'struct', 'switch', 'typedef', 'union', 'unsigned', 'void', 'volatile', 'while',
|
|
43
|
-
'include', 'define', 'main', 'printf', 'NULL'
|
|
53
|
+
'include', 'define', 'main', 'printf', 'NULL', '#include', '#define'
|
|
44
54
|
];
|
|
45
55
|
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
// --- PRNG: Xoshiro256** ---
|
|
57
|
+
class Xoshiro256 {
|
|
58
|
+
constructor(seedBuffer) {
|
|
59
|
+
if (seedBuffer.length < 32) throw new Error("Seed too short");
|
|
60
|
+
this.s = [
|
|
61
|
+
seedBuffer.readBigUInt64BE(0),
|
|
62
|
+
seedBuffer.readBigUInt64BE(8),
|
|
63
|
+
seedBuffer.readBigUInt64BE(16),
|
|
64
|
+
seedBuffer.readBigUInt64BE(24)
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
next() {
|
|
69
|
+
const result = this.rotl(this.s[1] * 5n, 7n) * 9n;
|
|
70
|
+
const t = this.s[1] << 17n;
|
|
71
|
+
|
|
72
|
+
this.s[2] ^= this.s[0];
|
|
73
|
+
this.s[3] ^= this.s[1];
|
|
74
|
+
this.s[1] ^= this.s[2];
|
|
75
|
+
this.s[0] ^= this.s[3];
|
|
48
76
|
|
|
49
|
-
|
|
77
|
+
this.s[2] ^= t;
|
|
78
|
+
this.s[3] = this.rotl(this.s[3], 45n);
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
nextFloat() {
|
|
84
|
+
const val = Number(this.next() >> 11n);
|
|
85
|
+
return val * (2 ** -53);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
rotl(x, k) {
|
|
89
|
+
return (x << k) | (x >> (64n - k));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
50
92
|
|
|
51
93
|
export class CipherEngine {
|
|
52
94
|
constructor(password) {
|
|
53
95
|
this.password = password;
|
|
54
|
-
this.
|
|
55
|
-
this.
|
|
96
|
+
this.keywordMap = new Map();
|
|
97
|
+
this.keywordReverseMap = new Map();
|
|
98
|
+
this.dataAlphabet = [];
|
|
99
|
+
this.dataReverseMap = new Map();
|
|
56
100
|
this.isReady = false;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
throw new Error(`CRITICAL: Not enough emojis. Need ${TOTAL_TOKENS}, have ${EMOJI_UNIVERSE.length}`);
|
|
60
|
-
}
|
|
101
|
+
this.hmac = null;
|
|
102
|
+
this.lineLength = 0; // For wrapping
|
|
61
103
|
}
|
|
62
104
|
|
|
63
|
-
async init(existingSaltHex = null) {
|
|
105
|
+
async init(existingSaltHex = null, expectedAuthCheck = null) {
|
|
64
106
|
this.salt = existingSaltHex
|
|
65
107
|
? Buffer.from(existingSaltHex, 'hex')
|
|
66
108
|
: crypto.randomBytes(16);
|
|
67
109
|
|
|
68
110
|
const derivedKey = await new Promise((resolve, reject) => {
|
|
69
|
-
crypto.pbkdf2(this.password, this.salt, 100000,
|
|
111
|
+
crypto.pbkdf2(this.password, this.salt, 100000, 64, 'sha512', (err, key) => {
|
|
70
112
|
if (err) reject(err); else resolve(key);
|
|
71
113
|
});
|
|
72
114
|
});
|
|
73
115
|
|
|
74
|
-
const seedBuffer = derivedKey.subarray(0,
|
|
75
|
-
this.
|
|
76
|
-
|
|
116
|
+
const seedBuffer = derivedKey.subarray(0, 32);
|
|
117
|
+
this.authKey = derivedKey.subarray(32, 64);
|
|
118
|
+
|
|
119
|
+
// Check password correctness immediately if Auth Check is provided
|
|
120
|
+
if (expectedAuthCheck) {
|
|
121
|
+
const calculatedAuthCheck = this.authKey.subarray(0, 4).toString('hex');
|
|
122
|
+
if (calculatedAuthCheck !== expectedAuthCheck) {
|
|
123
|
+
throw new Error("WRONG_PASSWORD");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
77
126
|
|
|
78
|
-
|
|
127
|
+
this.rng = new Xoshiro256(seedBuffer);
|
|
79
128
|
|
|
80
|
-
|
|
81
|
-
for (const keyword of C_KEYWORDS) {
|
|
82
|
-
const emo = shuffledEmojis[emojiIndex++];
|
|
83
|
-
this.tokenMap.set(keyword, emo);
|
|
84
|
-
this.reverseMap.set(emo, keyword);
|
|
85
|
-
}
|
|
129
|
+
const shuffled = this._shuffleArray([...RAW_UNIVERSE]);
|
|
86
130
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
131
|
+
let ptr = 0;
|
|
132
|
+
this.keywordEmojis = [];
|
|
133
|
+
for (const kw of C_KEYWORDS) {
|
|
134
|
+
const emo = shuffled[ptr++];
|
|
135
|
+
this.keywordEmojis.push(emo);
|
|
136
|
+
this.keywordMap.set(kw, emo);
|
|
137
|
+
this.keywordReverseMap.set(emo, kw);
|
|
91
138
|
}
|
|
92
139
|
|
|
93
|
-
this.
|
|
94
|
-
|
|
140
|
+
this.dataAlphabet = shuffled.slice(ptr, ptr + 1024);
|
|
141
|
+
if (this.dataAlphabet.length < 1024) throw new Error("Not enough emojis for Base-1024");
|
|
142
|
+
|
|
143
|
+
this.dataAlphabet.forEach((emo, idx) => {
|
|
144
|
+
this.dataReverseMap.set(emo, idx);
|
|
145
|
+
});
|
|
95
146
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
var t = a += 0x6D2B79F5;
|
|
99
|
-
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
100
|
-
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
101
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
102
|
-
}
|
|
147
|
+
this.hmac = crypto.createHmac('sha256', this.authKey);
|
|
148
|
+
this.isReady = true;
|
|
103
149
|
}
|
|
104
150
|
|
|
105
|
-
_shuffleArray(array
|
|
106
|
-
const rng = this._mulberry32(seed);
|
|
151
|
+
_shuffleArray(array) {
|
|
107
152
|
for (let i = array.length - 1; i > 0; i--) {
|
|
108
|
-
const j = Math.floor(rng() * (i + 1));
|
|
153
|
+
const j = Math.floor(this.rng.nextFloat() * (i + 1));
|
|
109
154
|
[array[i], array[j]] = [array[j], array[i]];
|
|
110
155
|
}
|
|
111
156
|
return array;
|
|
@@ -113,11 +158,10 @@ export class CipherEngine {
|
|
|
113
158
|
|
|
114
159
|
_encodeHeader() {
|
|
115
160
|
const saltHex = this.salt.toString('hex');
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
161
|
+
const authCheck = this.authKey.subarray(0, 4).toString('hex'); // 4 bytes check
|
|
162
|
+
|
|
119
163
|
let headerStr = '';
|
|
120
|
-
for (const char of
|
|
164
|
+
for (const char of (saltHex + authCheck)) {
|
|
121
165
|
const val = parseInt(char, 16);
|
|
122
166
|
headerStr += HEADER_ALPHABET[val];
|
|
123
167
|
}
|
|
@@ -126,7 +170,6 @@ export class CipherEngine {
|
|
|
126
170
|
|
|
127
171
|
static decodeHeader(headerStr) {
|
|
128
172
|
let hexString = '';
|
|
129
|
-
// Use segmenter here just in case moon emojis have variation selectors
|
|
130
173
|
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
131
174
|
const segments = segmenter.segment(headerStr.trim());
|
|
132
175
|
|
|
@@ -135,119 +178,232 @@ export class CipherEngine {
|
|
|
135
178
|
if (index === -1) throw new Error("Invalid Header format.");
|
|
136
179
|
hexString += index.toString(16);
|
|
137
180
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
saltHex: hexString.substring(0, 32),
|
|
184
|
+
authCheckHex: hexString.length >= 40 ? hexString.substring(32, 40) : null
|
|
185
|
+
};
|
|
141
186
|
}
|
|
142
187
|
|
|
188
|
+
// --- STREAMING ENCRYPTION ---
|
|
189
|
+
|
|
143
190
|
getEncryptStream() {
|
|
144
191
|
if (!this.isReady) throw new Error("Engine not initialized");
|
|
145
192
|
const engine = this;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
193
|
+
let buffer = Buffer.alloc(0);
|
|
194
|
+
|
|
149
195
|
return new Transform({
|
|
150
196
|
transform(chunk, encoding, callback) {
|
|
151
|
-
|
|
197
|
+
const str = chunk.toString('utf8');
|
|
152
198
|
|
|
153
199
|
const sortedKeywords = [...C_KEYWORDS].sort((a, b) => b.length - a.length);
|
|
154
200
|
const keywordPattern = sortedKeywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
155
|
-
const regex = new RegExp(
|
|
201
|
+
const regex = new RegExp(`(${keywordPattern})`, 'g');
|
|
156
202
|
|
|
157
|
-
|
|
158
|
-
let output = '';
|
|
203
|
+
const parts = str.split(regex);
|
|
159
204
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
205
|
+
for (const part of parts) {
|
|
206
|
+
if (!part) continue;
|
|
207
|
+
|
|
208
|
+
if (C_KEYWORDS.includes(part)) {
|
|
209
|
+
if (buffer.length > 0) {
|
|
210
|
+
this.push(engine._flushDataBuffer(buffer));
|
|
211
|
+
buffer = Buffer.alloc(0);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const baseIdx = engine.keywordEmojis.indexOf(engine.keywordMap.get(part));
|
|
215
|
+
const shift = Number(engine.rng.next() % BigInt(engine.keywordEmojis.length));
|
|
216
|
+
const newIdx = (baseIdx + shift) % engine.keywordEmojis.length;
|
|
217
|
+
const polyEmoji = engine.keywordEmojis[newIdx];
|
|
218
|
+
|
|
219
|
+
const outBuf = Buffer.from(polyEmoji);
|
|
220
|
+
engine.hmac.update(outBuf);
|
|
221
|
+
|
|
222
|
+
// Emit wrapped
|
|
223
|
+
this.push(engine._wrapOutput(outBuf));
|
|
224
|
+
|
|
164
225
|
} else {
|
|
165
|
-
|
|
226
|
+
buffer = Buffer.concat([buffer, Buffer.from(part, 'utf8')]);
|
|
227
|
+
|
|
228
|
+
while (buffer.length >= 5) {
|
|
229
|
+
const chunk = buffer.subarray(0, 5);
|
|
230
|
+
buffer = buffer.subarray(5);
|
|
231
|
+
|
|
232
|
+
const enc = engine._encodeBase1024(chunk);
|
|
233
|
+
engine.hmac.update(enc);
|
|
234
|
+
this.push(engine._wrapOutput(enc));
|
|
235
|
+
}
|
|
166
236
|
}
|
|
167
237
|
}
|
|
168
|
-
|
|
169
|
-
buffer = '';
|
|
170
|
-
this.push(output);
|
|
171
238
|
callback();
|
|
172
239
|
},
|
|
240
|
+
|
|
173
241
|
flush(callback) {
|
|
174
|
-
buffer
|
|
175
|
-
|
|
242
|
+
if (buffer.length > 0) {
|
|
243
|
+
const padded = Buffer.alloc(5);
|
|
244
|
+
buffer.copy(padded);
|
|
245
|
+
const enc = engine._encodeBase1024(padded);
|
|
246
|
+
engine.hmac.update(enc);
|
|
247
|
+
this.push(engine._wrapOutput(enc));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const digest = engine.hmac.digest();
|
|
251
|
+
let footerStr = '';
|
|
252
|
+
for (const byte of digest) {
|
|
253
|
+
const hex = byte.toString(16).padStart(2, '0');
|
|
254
|
+
for (const char of hex) {
|
|
255
|
+
const val = parseInt(char, 16);
|
|
256
|
+
footerStr += HEADER_ALPHABET[val];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Push footer (always starts on a new line or wrapped)
|
|
260
|
+
this.push(Buffer.from('\n' + footerStr));
|
|
176
261
|
callback();
|
|
177
262
|
}
|
|
178
263
|
});
|
|
179
264
|
}
|
|
180
265
|
|
|
266
|
+
_wrapOutput(bufferChunk) {
|
|
267
|
+
this.lineLength += bufferChunk.length;
|
|
268
|
+
if (this.lineLength > 300) {
|
|
269
|
+
this.lineLength = 0;
|
|
270
|
+
return Buffer.concat([bufferChunk, Buffer.from('\n')]);
|
|
271
|
+
}
|
|
272
|
+
return bufferChunk;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
_encodeBase1024(buffer5) {
|
|
276
|
+
let val = 0n;
|
|
277
|
+
for (let i = 0; i < 5; i++) {
|
|
278
|
+
val += BigInt(buffer5[i]) * (256n ** BigInt(i));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let output = '';
|
|
282
|
+
for (let i = 0; i < 4; i++) {
|
|
283
|
+
const idx = Number(val % 1024n);
|
|
284
|
+
val = val / 1024n;
|
|
285
|
+
output += this.dataAlphabet[idx];
|
|
286
|
+
}
|
|
287
|
+
return Buffer.from(output);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_flushDataBuffer(buf) {
|
|
291
|
+
if (buf.length === 0) return Buffer.alloc(0);
|
|
292
|
+
|
|
293
|
+
const padded = Buffer.alloc(5);
|
|
294
|
+
buf.copy(padded);
|
|
295
|
+
const enc = this._encodeBase1024(padded);
|
|
296
|
+
this.hmac.update(enc);
|
|
297
|
+
return this._wrapOutput(enc);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// --- STREAMING DECRYPTION ---
|
|
301
|
+
|
|
181
302
|
getDecryptStream() {
|
|
182
303
|
if (!this.isReady) throw new Error("Engine not initialized");
|
|
183
304
|
const engine = this;
|
|
184
|
-
const decoder = new StringDecoder('utf8');
|
|
185
305
|
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
186
306
|
|
|
187
|
-
|
|
307
|
+
const FOOTER_LEN = 64;
|
|
308
|
+
let emojiBuffer = [];
|
|
188
309
|
|
|
189
310
|
return new Transform({
|
|
190
311
|
transform(chunk, encoding, callback) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// 2. Segment into Graphemes (Emojis)
|
|
195
|
-
const segments = [...segmenter.segment(buffer)];
|
|
312
|
+
const str = chunk.toString('utf8');
|
|
313
|
+
const segments = [...segmenter.segment(str)];
|
|
196
314
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// (e.g. Base char is here, Variation Selector is in next chunk)
|
|
201
|
-
|
|
202
|
-
const processUntilIndex = segments.length > 1 ? segments.length - 1 : 0;
|
|
203
|
-
let output = '';
|
|
204
|
-
let processedString = '';
|
|
205
|
-
|
|
206
|
-
// If we only have 1 segment, we can't be sure it's complete, wait for next chunk
|
|
207
|
-
// UNLESS the buffer is getting huge, then force it.
|
|
208
|
-
if (segments.length === 1 && buffer.length < 100) {
|
|
209
|
-
// Wait for more data
|
|
210
|
-
callback();
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
315
|
+
for (const { segment } of segments) {
|
|
316
|
+
// Filter out whitespace/newlines used for wrapping
|
|
317
|
+
if (segment.match(/\s/)) continue;
|
|
213
318
|
|
|
214
|
-
|
|
215
|
-
for (let i = 0; i < processUntilIndex; i++) {
|
|
216
|
-
const char = segments[i].segment;
|
|
217
|
-
processedString += char;
|
|
319
|
+
emojiBuffer.push(segment);
|
|
218
320
|
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
321
|
+
if (emojiBuffer.length > FOOTER_LEN) {
|
|
322
|
+
const emo = emojiBuffer.shift();
|
|
323
|
+
engine._processDecryptToken(emo, this);
|
|
324
|
+
engine.hmac.update(Buffer.from(emo));
|
|
223
325
|
}
|
|
224
326
|
}
|
|
225
|
-
|
|
226
|
-
// Update buffer to only contain the remaining tail
|
|
227
|
-
// Note: buffer might contain bytes not yet in segments if StringDecoder held them?
|
|
228
|
-
// No, decoder.write returns what is available.
|
|
229
|
-
// We just need to remove the processed part from the buffer string.
|
|
230
|
-
if (processUntilIndex > 0) {
|
|
231
|
-
buffer = buffer.slice(processedString.length);
|
|
232
|
-
this.push(output);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
327
|
callback();
|
|
236
328
|
},
|
|
329
|
+
|
|
237
330
|
flush(callback) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
331
|
+
if (emojiBuffer.length !== FOOTER_LEN) {
|
|
332
|
+
this.emit('error', new Error("File corrupted or truncated (No Footer)"));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const footerStr = emojiBuffer.join('');
|
|
337
|
+
const calcDigest = engine.hmac.digest('hex');
|
|
338
|
+
|
|
339
|
+
let footerHex = '';
|
|
340
|
+
try {
|
|
341
|
+
for (const char of footerStr) {
|
|
342
|
+
const idx = HEADER_ALPHABET.indexOf(char);
|
|
343
|
+
if (idx === -1) throw new Error();
|
|
344
|
+
footerHex += idx.toString(16);
|
|
246
345
|
}
|
|
247
|
-
|
|
346
|
+
} catch (e) {
|
|
347
|
+
this.emit('error', new Error("Invalid Integrity Seal"));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (footerHex !== calcDigest) {
|
|
352
|
+
// Critical security failure
|
|
353
|
+
this.emit('error', new Error("FILE_TAMPERED"));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (engine.decodeDataBuf.length > 0) {
|
|
248
358
|
}
|
|
249
359
|
callback();
|
|
250
360
|
}
|
|
251
361
|
});
|
|
252
362
|
}
|
|
363
|
+
|
|
364
|
+
decodeDataBuf = [];
|
|
365
|
+
|
|
366
|
+
_processDecryptToken(emo, stream) {
|
|
367
|
+
if (this.keywordReverseMap.has(emo)) {
|
|
368
|
+
const currentR = Number(this.rng.next() % BigInt(this.keywordEmojis.length));
|
|
369
|
+
const emoIdx = this.keywordEmojis.indexOf(emo);
|
|
370
|
+
|
|
371
|
+
let baseIdx = (emoIdx - currentR) % this.keywordEmojis.length;
|
|
372
|
+
if (baseIdx < 0) baseIdx += this.keywordEmojis.length;
|
|
373
|
+
|
|
374
|
+
const originalEmo = this.keywordEmojis[baseIdx];
|
|
375
|
+
const keyword = this.keywordReverseMap.get(originalEmo);
|
|
376
|
+
|
|
377
|
+
if (this.decodeDataBuf.length > 0) {
|
|
378
|
+
this.decodeDataBuf = [];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
stream.push(keyword);
|
|
382
|
+
|
|
383
|
+
} else if (this.dataReverseMap.has(emo)) {
|
|
384
|
+
this.decodeDataBuf.push(this.dataReverseMap.get(emo));
|
|
385
|
+
if (this.decodeDataBuf.length === 4) {
|
|
386
|
+
const chunk = this._decodeBase1024(this.decodeDataBuf);
|
|
387
|
+
this.decodeDataBuf = [];
|
|
388
|
+
const cleanChunk = chunk.filter(b => b !== 0x00);
|
|
389
|
+
stream.push(cleanChunk);
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
// Unknown emojis are ignored now
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
_decodeBase1024(indices) {
|
|
397
|
+
let val = 0n;
|
|
398
|
+
for (let i = 3; i >= 0; i--) {
|
|
399
|
+
val = (val * 1024n) + BigInt(indices[i]);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const buf = Buffer.alloc(5);
|
|
403
|
+
for (let i = 0; i < 5; i++) {
|
|
404
|
+
buf[i] = Number(val % 256n);
|
|
405
|
+
val = val / 256n;
|
|
406
|
+
}
|
|
407
|
+
return buf;
|
|
408
|
+
}
|
|
253
409
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mojic",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Obfuscate C source code into
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Obfuscate C source code into encrypted, password-seeded emoji streams.",
|
|
5
5
|
"main": "bin/mojic.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mojic": "bin/mojic.js"
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"start": "node bin/mojic.js"
|
|
11
|
+
"start": "node bin/mojic.js",
|
|
12
|
+
"build-binaries": "pkg . --out-path dist --public"
|
|
12
13
|
},
|
|
13
14
|
"repository": {
|
|
14
15
|
"type": "git",
|
|
@@ -19,6 +20,9 @@
|
|
|
19
20
|
"commander": "^11.1.0",
|
|
20
21
|
"inquirer": "^9.2.12"
|
|
21
22
|
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"pkg": "^5.8.1"
|
|
25
|
+
},
|
|
22
26
|
"engines": {
|
|
23
27
|
"node": ">=18.0.0"
|
|
24
28
|
},
|
|
@@ -28,8 +32,22 @@
|
|
|
28
32
|
"emoji",
|
|
29
33
|
"obfuscation",
|
|
30
34
|
"c",
|
|
31
|
-
"security"
|
|
35
|
+
"security",
|
|
36
|
+
"polymorphic"
|
|
32
37
|
],
|
|
33
38
|
"author": "notamitgamer",
|
|
34
|
-
"license": "Apache-2.0"
|
|
39
|
+
"license": "Apache-2.0",
|
|
40
|
+
"pkg": {
|
|
41
|
+
"scripts": "bin/mojic.js",
|
|
42
|
+
"assets": [
|
|
43
|
+
"lib/**/*",
|
|
44
|
+
"package.json"
|
|
45
|
+
],
|
|
46
|
+
"targets": [
|
|
47
|
+
"node18-win-x64",
|
|
48
|
+
"node18-linux-x64",
|
|
49
|
+
"node18-macos-x64"
|
|
50
|
+
],
|
|
51
|
+
"outputPath": "dist"
|
|
52
|
+
}
|
|
35
53
|
}
|