mojic 1.0.3 → 1.2.4
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/CONTRIBUTING.md +43 -11
- package/README.md +53 -23
- package/SECURITY.md +2 -2
- package/bin/mojic.js +76 -93
- package/lib/CipherEngine.js +288 -142
- package/package.json +28 -6
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.2.4
|
|
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.
|
|
15
|
+
.version('1.2.4')
|
|
16
16
|
.addHelpCommand('help [command]', 'Display help for command')
|
|
17
17
|
.showHelpAfterError();
|
|
18
18
|
|
|
@@ -78,11 +78,20 @@ const createHeaderSkipper = () => {
|
|
|
78
78
|
});
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
const createMinifier = () => {
|
|
82
|
+
return new Transform({
|
|
83
|
+
transform(chunk, encoding, cb) {
|
|
84
|
+
let str = chunk.toString();
|
|
85
|
+
str = str.replace(/\r?\n|\r/g, ' ');
|
|
86
|
+
str = str.replace(/\s+/g, ' ');
|
|
87
|
+
this.push(str);
|
|
88
|
+
cb();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
};
|
|
82
92
|
|
|
83
93
|
async function traverseDirectory(currentPath, extension, callback) {
|
|
84
94
|
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
85
|
-
|
|
86
95
|
for (const entry of entries) {
|
|
87
96
|
const fullPath = path.join(currentPath, entry.name);
|
|
88
97
|
if (entry.isDirectory()) {
|
|
@@ -99,18 +108,20 @@ program
|
|
|
99
108
|
.command('encode')
|
|
100
109
|
.argument('<path>', 'File or Directory to encode')
|
|
101
110
|
.option('-r, --recursive', 'Recursively encrypt all .c files in directory')
|
|
111
|
+
.option('-f, --flat', 'Flatten structure (strip whitespace/newlines) before encrypting')
|
|
102
112
|
.description('Encrypt a file or directory into emojis')
|
|
103
113
|
.action(async (targetPath, options) => {
|
|
104
114
|
try {
|
|
105
115
|
if (!fs.existsSync(targetPath)) throw new Error('Path not found');
|
|
106
116
|
const stats = fs.statSync(targetPath);
|
|
107
117
|
|
|
108
|
-
// Validation
|
|
109
118
|
if (stats.isDirectory() && !options.recursive) {
|
|
110
119
|
throw new Error(`'${targetPath}' is a directory. Use -r to process recursively.`);
|
|
111
120
|
}
|
|
112
121
|
|
|
113
|
-
console.log(chalk.blue('
|
|
122
|
+
console.log(chalk.blue('Initiating Mojic Encryption v1.2...'));
|
|
123
|
+
if (options.flat) console.log(chalk.yellow(' -> Structural Flattening Enabled'));
|
|
124
|
+
|
|
114
125
|
const password = await promptPassword('Create password for file(s):');
|
|
115
126
|
|
|
116
127
|
const processFile = async (filePath) => {
|
|
@@ -118,7 +129,7 @@ program
|
|
|
118
129
|
console.log(chalk.dim(` Processing: ${path.basename(filePath)} -> ${path.basename(outputName)}`));
|
|
119
130
|
|
|
120
131
|
const engine = new CipherEngine(password);
|
|
121
|
-
await engine.init();
|
|
132
|
+
await engine.init();
|
|
122
133
|
|
|
123
134
|
const readStream = fs.createReadStream(filePath);
|
|
124
135
|
const writeStream = fs.createWriteStream(outputName);
|
|
@@ -126,7 +137,10 @@ program
|
|
|
126
137
|
writeStream.write(engine._encodeHeader());
|
|
127
138
|
|
|
128
139
|
await new Promise((resolve, reject) => {
|
|
129
|
-
readStream
|
|
140
|
+
let pipeline = readStream;
|
|
141
|
+
if (options.flat) pipeline = pipeline.pipe(createMinifier());
|
|
142
|
+
|
|
143
|
+
pipeline
|
|
130
144
|
.pipe(engine.getEncryptStream())
|
|
131
145
|
.pipe(writeStream)
|
|
132
146
|
.on('finish', resolve)
|
|
@@ -135,12 +149,12 @@ program
|
|
|
135
149
|
};
|
|
136
150
|
|
|
137
151
|
if (stats.isDirectory()) {
|
|
138
|
-
console.log(chalk.blue(
|
|
152
|
+
console.log(chalk.blue(`Scanning directory: ${targetPath}`));
|
|
139
153
|
await traverseDirectory(targetPath, '.c', processFile);
|
|
140
|
-
console.log(chalk.green('
|
|
154
|
+
console.log(chalk.green('Batch encryption complete.'));
|
|
141
155
|
} else {
|
|
142
156
|
await processFile(targetPath);
|
|
143
|
-
console.log(chalk.green(
|
|
157
|
+
console.log(chalk.green(`Encrypted.`));
|
|
144
158
|
}
|
|
145
159
|
|
|
146
160
|
} catch (err) {
|
|
@@ -162,8 +176,8 @@ program
|
|
|
162
176
|
throw new Error(`'${targetPath}' is a directory. Use -r to process recursively.`);
|
|
163
177
|
}
|
|
164
178
|
|
|
165
|
-
console.log(chalk.blue('
|
|
166
|
-
const password = await promptPassword('Enter password:');
|
|
179
|
+
console.log(chalk.blue('Initiating Decryption...'));
|
|
180
|
+
const password = await promptPassword('Enter password:');
|
|
167
181
|
|
|
168
182
|
const processFile = async (filePath) => {
|
|
169
183
|
try {
|
|
@@ -172,38 +186,51 @@ program
|
|
|
172
186
|
|
|
173
187
|
const headerStr = await getStreamHeader(filePath);
|
|
174
188
|
const metadata = CipherEngine.decodeHeader(headerStr);
|
|
175
|
-
|
|
176
189
|
const engine = new CipherEngine(password);
|
|
177
|
-
await engine.init(metadata.saltHex);
|
|
178
|
-
|
|
179
|
-
if (engine.authHash.toString('hex') !== metadata.authHex) {
|
|
180
|
-
console.log(chalk.red(` ❌ Skipped ${path.basename(filePath)}: Incorrect Password`));
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
190
|
+
await engine.init(metadata.saltHex, metadata.authCheckHex);
|
|
183
191
|
|
|
184
192
|
const readStream = fs.createReadStream(filePath);
|
|
185
193
|
const writeStream = fs.createWriteStream(outputName);
|
|
186
194
|
|
|
187
195
|
await new Promise((resolve, reject) => {
|
|
196
|
+
const decryptStream = engine.getDecryptStream();
|
|
197
|
+
decryptStream.on('error', (err) => {
|
|
198
|
+
readStream.destroy(); // Stop reading
|
|
199
|
+
writeStream.destroy(); // Stop writing
|
|
200
|
+
reject(err);
|
|
201
|
+
});
|
|
202
|
+
|
|
188
203
|
readStream
|
|
189
204
|
.pipe(createHeaderSkipper())
|
|
190
|
-
.pipe(
|
|
205
|
+
.pipe(decryptStream)
|
|
191
206
|
.pipe(writeStream)
|
|
192
207
|
.on('finish', resolve)
|
|
193
208
|
.on('error', reject);
|
|
194
209
|
});
|
|
210
|
+
return true;
|
|
195
211
|
} catch (e) {
|
|
196
|
-
|
|
212
|
+
const outputName = filePath.replace(/\.mojic$/, '') + '.restored.c';
|
|
213
|
+
// Wait for stream handles to release
|
|
214
|
+
setTimeout(() => { if (fs.existsSync(outputName)) try { fs.unlinkSync(outputName); } catch(ign){} }, 100);
|
|
215
|
+
|
|
216
|
+
if (e.message === "WRONG_PASSWORD") {
|
|
217
|
+
console.log(chalk.red(` Error: Incorrect Password`));
|
|
218
|
+
} else if (e.message === "FILE_TAMPERED") {
|
|
219
|
+
console.log(chalk.red(` Error: File Tampered! Integrity check failed.`));
|
|
220
|
+
} else {
|
|
221
|
+
console.log(chalk.red(` Error: ${e.message}`));
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
197
224
|
}
|
|
198
225
|
};
|
|
199
226
|
|
|
200
227
|
if (stats.isDirectory()) {
|
|
201
|
-
console.log(chalk.blue(
|
|
228
|
+
console.log(chalk.blue(`Scanning directory: ${targetPath}`));
|
|
202
229
|
await traverseDirectory(targetPath, '.mojic', processFile);
|
|
203
|
-
console.log(chalk.green('
|
|
230
|
+
console.log(chalk.green('Batch decryption complete.'));
|
|
204
231
|
} else {
|
|
205
|
-
await processFile(targetPath);
|
|
206
|
-
console.log(chalk.green(
|
|
232
|
+
const success = await processFile(targetPath);
|
|
233
|
+
if (success) console.log(chalk.green(`Restored.`));
|
|
207
234
|
}
|
|
208
235
|
|
|
209
236
|
} catch (err) {
|
|
@@ -211,32 +238,23 @@ program
|
|
|
211
238
|
}
|
|
212
239
|
});
|
|
213
240
|
|
|
214
|
-
// ---
|
|
215
|
-
|
|
241
|
+
// --- SRT ---
|
|
216
242
|
const rotatePassword = async (file) => {
|
|
217
243
|
try {
|
|
218
244
|
if (!fs.existsSync(file)) throw new Error('File not found');
|
|
219
|
-
console.log(chalk.yellow(
|
|
245
|
+
console.log(chalk.yellow(`Rotating Password for ${path.basename(file)}...`));
|
|
220
246
|
|
|
221
|
-
// 1. Authenticate Old
|
|
222
247
|
const headerStr = await getStreamHeader(file);
|
|
223
248
|
const metadata = CipherEngine.decodeHeader(headerStr);
|
|
224
249
|
const oldPass = await promptPassword('Enter CURRENT password:');
|
|
225
250
|
|
|
226
251
|
const oldEngine = new CipherEngine(oldPass);
|
|
227
|
-
await oldEngine.init(metadata.saltHex);
|
|
228
|
-
|
|
229
|
-
if (oldEngine.authHash.toString('hex') !== metadata.authHex) {
|
|
230
|
-
console.error(chalk.red('❌ Incorrect Current Password'));
|
|
231
|
-
process.exit(1);
|
|
232
|
-
}
|
|
252
|
+
await oldEngine.init(metadata.saltHex, metadata.authCheckHex);
|
|
233
253
|
|
|
234
|
-
// 2. Init New
|
|
235
254
|
const newPass = await promptPassword('Enter NEW password:');
|
|
236
255
|
const newEngine = new CipherEngine(newPass);
|
|
237
256
|
await newEngine.init();
|
|
238
257
|
|
|
239
|
-
// 3. Process
|
|
240
258
|
const tempFile = file + '.tmp';
|
|
241
259
|
const readStream = fs.createReadStream(file);
|
|
242
260
|
const writeStream = fs.createWriteStream(tempFile);
|
|
@@ -244,46 +262,36 @@ const rotatePassword = async (file) => {
|
|
|
244
262
|
writeStream.write(newEngine._encodeHeader());
|
|
245
263
|
|
|
246
264
|
await new Promise((resolve, reject) => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
.pipe(newEngine.getEncryptStream())
|
|
251
|
-
.pipe(writeStream)
|
|
252
|
-
.on('finish', resolve)
|
|
253
|
-
.on('error', reject);
|
|
265
|
+
const decryptStream = oldEngine.getDecryptStream();
|
|
266
|
+
decryptStream.on('error', reject);
|
|
267
|
+
readStream.pipe(createHeaderSkipper()).pipe(decryptStream).pipe(newEngine.getEncryptStream()).pipe(writeStream).on('finish', resolve).on('error', reject);
|
|
254
268
|
});
|
|
255
269
|
|
|
256
270
|
fs.renameSync(tempFile, file);
|
|
257
|
-
console.log(chalk.green(
|
|
271
|
+
console.log(chalk.green(`Password updated.`));
|
|
258
272
|
|
|
259
273
|
} catch (err) {
|
|
260
|
-
|
|
274
|
+
if (fs.existsSync(file + '.tmp')) fs.unlinkSync(file + '.tmp');
|
|
275
|
+
if (err.message === "WRONG_PASSWORD") console.error(chalk.red('Error: Incorrect Current Password'));
|
|
276
|
+
else console.error(chalk.red('Error:'), err.message);
|
|
261
277
|
}
|
|
262
278
|
};
|
|
263
279
|
|
|
264
280
|
const reEncrypt = async (file) => {
|
|
265
281
|
try {
|
|
266
282
|
if (!fs.existsSync(file)) throw new Error('File not found');
|
|
267
|
-
console.log(chalk.yellow(
|
|
283
|
+
console.log(chalk.yellow(`Re-shuffling Entropy for ${path.basename(file)}...`));
|
|
268
284
|
|
|
269
|
-
// 1. Authenticate
|
|
270
285
|
const headerStr = await getStreamHeader(file);
|
|
271
286
|
const metadata = CipherEngine.decodeHeader(headerStr);
|
|
272
287
|
const password = await promptPassword('Enter password:');
|
|
273
288
|
|
|
274
289
|
const oldEngine = new CipherEngine(password);
|
|
275
|
-
await oldEngine.init(metadata.saltHex);
|
|
290
|
+
await oldEngine.init(metadata.saltHex, metadata.authCheckHex);
|
|
276
291
|
|
|
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
292
|
const newEngine = new CipherEngine(password);
|
|
284
293
|
await newEngine.init();
|
|
285
294
|
|
|
286
|
-
// 3. Process
|
|
287
295
|
const tempFile = file + '.tmp';
|
|
288
296
|
const readStream = fs.createReadStream(file);
|
|
289
297
|
const writeStream = fs.createWriteStream(tempFile);
|
|
@@ -291,50 +299,25 @@ const reEncrypt = async (file) => {
|
|
|
291
299
|
writeStream.write(newEngine._encodeHeader());
|
|
292
300
|
|
|
293
301
|
await new Promise((resolve, reject) => {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
.pipe(newEngine.getEncryptStream())
|
|
298
|
-
.pipe(writeStream)
|
|
299
|
-
.on('finish', resolve)
|
|
300
|
-
.on('error', reject);
|
|
302
|
+
const decryptStream = oldEngine.getDecryptStream();
|
|
303
|
+
decryptStream.on('error', reject);
|
|
304
|
+
readStream.pipe(createHeaderSkipper()).pipe(decryptStream).pipe(newEngine.getEncryptStream()).pipe(writeStream).on('finish', resolve).on('error', reject);
|
|
301
305
|
});
|
|
302
306
|
|
|
303
307
|
fs.renameSync(tempFile, file);
|
|
304
|
-
console.log(chalk.green(
|
|
308
|
+
console.log(chalk.green(`File re-encrypted.`));
|
|
305
309
|
|
|
306
310
|
} catch (err) {
|
|
307
|
-
|
|
311
|
+
if (fs.existsSync(file + '.tmp')) fs.unlinkSync(file + '.tmp');
|
|
312
|
+
if (err.message === "WRONG_PASSWORD") console.error(chalk.red('Error: Incorrect Password'));
|
|
313
|
+
else console.error(chalk.red('Error:'), err.message);
|
|
308
314
|
}
|
|
309
315
|
};
|
|
310
316
|
|
|
311
|
-
program
|
|
312
|
-
.
|
|
313
|
-
.
|
|
314
|
-
.
|
|
315
|
-
|
|
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
|
-
.action(async (options) => {
|
|
322
|
-
if (options.pass) {
|
|
323
|
-
await rotatePassword(options.pass);
|
|
324
|
-
} else if (options.re) {
|
|
325
|
-
await reEncrypt(options.re);
|
|
326
|
-
} else {
|
|
327
|
-
console.log(chalk.yellow('Please specify an option: --pass <file> or --re <file>'));
|
|
328
|
-
program.commands.find(c => c.name() === 'srt').help();
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
|
|
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
|
-
`);
|
|
317
|
+
program.command('srt').description('Security and Rotation Tools').option('--pass <file>', 'Update password').option('--re <file>', 'Re-encrypt').action(async (options) => {
|
|
318
|
+
if (options.pass) await rotatePassword(options.pass);
|
|
319
|
+
else if (options.re) await reEncrypt(options.re);
|
|
320
|
+
else { console.log(chalk.yellow('Please specify an option: --pass <file> or --re <file>')); program.commands.find(c => c.name() === 'srt').help(); }
|
|
321
|
+
});
|
|
339
322
|
|
|
340
323
|
program.parse(process.argv);
|
package/lib/CipherEngine.js
CHANGED
|
@@ -3,109 +3,150 @@ import { Transform } from 'stream';
|
|
|
3
3
|
import { StringDecoder } from 'string_decoder';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* *
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
6
|
+
* MOJIC v1.2.4 CIPHER ENGINE
|
|
7
|
+
* "Operation Polymorphic Chaos"
|
|
8
|
+
* * Fixes:
|
|
9
|
+
* - Word Boundaries: Prevents splitting variables like 'secretCode'
|
|
10
|
+
* - Regex: correctly handles #directives vs keywords
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
// --- EMOJI UNIVERSE GENERATION ---
|
|
13
14
|
const HEADER_ALPHABET = ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗'];
|
|
14
15
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
16
|
+
const generateUniverse = () => {
|
|
17
|
+
const universe = [];
|
|
18
|
+
const ranges = [
|
|
19
|
+
[0x1F600, 0x1F64F], // Emoticons
|
|
20
|
+
[0x1F300, 0x1F5FF], // Misc Symbols
|
|
21
|
+
[0x1F680, 0x1F6FF], // Transport
|
|
22
|
+
[0x1F900, 0x1F9FF] // Supplemental
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const headerSet = new Set(HEADER_ALPHABET);
|
|
26
|
+
|
|
27
|
+
for (const [start, end] of ranges) {
|
|
28
|
+
for (let code = start; code <= end; code++) {
|
|
29
|
+
const char = String.fromCodePoint(code);
|
|
30
|
+
if (!headerSet.has(char)) {
|
|
31
|
+
universe.push(char);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return universe; // Expect > 1100 chars
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const RAW_UNIVERSE = generateUniverse();
|
|
39
|
+
|
|
40
|
+
if (RAW_UNIVERSE.length < 1080) {
|
|
41
|
+
throw new Error("Critical: Emoji Universe generation failed to produce enough tokens.");
|
|
42
|
+
}
|
|
37
43
|
|
|
38
44
|
const C_KEYWORDS = [
|
|
39
45
|
'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do',
|
|
40
46
|
'double', 'else', 'enum', 'extern', 'float', 'for', 'goto', 'if',
|
|
41
47
|
'int', 'long', 'register', 'return', 'short', 'signed', 'sizeof', 'static',
|
|
42
48
|
'struct', 'switch', 'typedef', 'union', 'unsigned', 'void', 'volatile', 'while',
|
|
43
|
-
'include', 'define', 'main', 'printf', 'NULL'
|
|
49
|
+
'include', 'define', 'main', 'printf', 'NULL', '#include', '#define'
|
|
44
50
|
];
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
// --- PRNG: Xoshiro256** ---
|
|
53
|
+
class Xoshiro256 {
|
|
54
|
+
constructor(seedBuffer) {
|
|
55
|
+
if (seedBuffer.length < 32) throw new Error("Seed too short");
|
|
56
|
+
this.s = [
|
|
57
|
+
seedBuffer.readBigUInt64BE(0),
|
|
58
|
+
seedBuffer.readBigUInt64BE(8),
|
|
59
|
+
seedBuffer.readBigUInt64BE(16),
|
|
60
|
+
seedBuffer.readBigUInt64BE(24)
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
next() {
|
|
65
|
+
const result = this.rotl(this.s[1] * 5n, 7n) * 9n;
|
|
66
|
+
const t = this.s[1] << 17n;
|
|
48
67
|
|
|
49
|
-
|
|
68
|
+
this.s[2] ^= this.s[0];
|
|
69
|
+
this.s[3] ^= this.s[1];
|
|
70
|
+
this.s[1] ^= this.s[2];
|
|
71
|
+
this.s[0] ^= this.s[3];
|
|
72
|
+
|
|
73
|
+
this.s[2] ^= t;
|
|
74
|
+
this.s[3] = this.rotl(this.s[3], 45n);
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
nextFloat() {
|
|
80
|
+
const val = Number(this.next() >> 11n);
|
|
81
|
+
return val * (2 ** -53);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
rotl(x, k) {
|
|
85
|
+
return (x << k) | (x >> (64n - k));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
50
88
|
|
|
51
89
|
export class CipherEngine {
|
|
52
90
|
constructor(password) {
|
|
53
91
|
this.password = password;
|
|
54
|
-
this.
|
|
55
|
-
this.
|
|
92
|
+
this.keywordMap = new Map();
|
|
93
|
+
this.keywordReverseMap = new Map();
|
|
94
|
+
this.dataAlphabet = [];
|
|
95
|
+
this.dataReverseMap = new Map();
|
|
56
96
|
this.isReady = false;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
throw new Error(`CRITICAL: Not enough emojis. Need ${TOTAL_TOKENS}, have ${EMOJI_UNIVERSE.length}`);
|
|
60
|
-
}
|
|
97
|
+
this.hmac = null;
|
|
98
|
+
this.lineLength = 0; // For wrapping
|
|
61
99
|
}
|
|
62
100
|
|
|
63
|
-
async init(existingSaltHex = null) {
|
|
101
|
+
async init(existingSaltHex = null, expectedAuthCheck = null) {
|
|
64
102
|
this.salt = existingSaltHex
|
|
65
103
|
? Buffer.from(existingSaltHex, 'hex')
|
|
66
104
|
: crypto.randomBytes(16);
|
|
67
105
|
|
|
68
106
|
const derivedKey = await new Promise((resolve, reject) => {
|
|
69
|
-
crypto.pbkdf2(this.password, this.salt, 100000,
|
|
107
|
+
crypto.pbkdf2(this.password, this.salt, 100000, 64, 'sha512', (err, key) => {
|
|
70
108
|
if (err) reject(err); else resolve(key);
|
|
71
109
|
});
|
|
72
110
|
});
|
|
73
111
|
|
|
74
|
-
const seedBuffer = derivedKey.subarray(0,
|
|
75
|
-
this.
|
|
76
|
-
|
|
112
|
+
const seedBuffer = derivedKey.subarray(0, 32);
|
|
113
|
+
this.authKey = derivedKey.subarray(32, 64);
|
|
114
|
+
|
|
115
|
+
// Check password correctness immediately if Auth Check is provided
|
|
116
|
+
if (expectedAuthCheck) {
|
|
117
|
+
const calculatedAuthCheck = this.authKey.subarray(0, 4).toString('hex');
|
|
118
|
+
if (calculatedAuthCheck !== expectedAuthCheck) {
|
|
119
|
+
throw new Error("WRONG_PASSWORD");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.rng = new Xoshiro256(seedBuffer);
|
|
77
124
|
|
|
78
|
-
const
|
|
125
|
+
const shuffled = this._shuffleArray([...RAW_UNIVERSE]);
|
|
79
126
|
|
|
80
|
-
let
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
this.
|
|
127
|
+
let ptr = 0;
|
|
128
|
+
this.keywordEmojis = [];
|
|
129
|
+
for (const kw of C_KEYWORDS) {
|
|
130
|
+
const emo = shuffled[ptr++];
|
|
131
|
+
this.keywordEmojis.push(emo);
|
|
132
|
+
this.keywordMap.set(kw, emo);
|
|
133
|
+
this.keywordReverseMap.set(emo, kw);
|
|
85
134
|
}
|
|
86
135
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
136
|
+
this.dataAlphabet = shuffled.slice(ptr, ptr + 1024);
|
|
137
|
+
if (this.dataAlphabet.length < 1024) throw new Error("Not enough emojis for Base-1024");
|
|
138
|
+
|
|
139
|
+
this.dataAlphabet.forEach((emo, idx) => {
|
|
140
|
+
this.dataReverseMap.set(emo, idx);
|
|
141
|
+
});
|
|
92
142
|
|
|
143
|
+
this.hmac = crypto.createHmac('sha256', this.authKey);
|
|
93
144
|
this.isReady = true;
|
|
94
145
|
}
|
|
95
146
|
|
|
96
|
-
|
|
97
|
-
return function() {
|
|
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
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
_shuffleArray(array, seed) {
|
|
106
|
-
const rng = this._mulberry32(seed);
|
|
147
|
+
_shuffleArray(array) {
|
|
107
148
|
for (let i = array.length - 1; i > 0; i--) {
|
|
108
|
-
const j = Math.floor(rng() * (i + 1));
|
|
149
|
+
const j = Math.floor(this.rng.nextFloat() * (i + 1));
|
|
109
150
|
[array[i], array[j]] = [array[j], array[i]];
|
|
110
151
|
}
|
|
111
152
|
return array;
|
|
@@ -113,11 +154,10 @@ export class CipherEngine {
|
|
|
113
154
|
|
|
114
155
|
_encodeHeader() {
|
|
115
156
|
const saltHex = this.salt.toString('hex');
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
157
|
+
const authCheck = this.authKey.subarray(0, 4).toString('hex');
|
|
158
|
+
|
|
119
159
|
let headerStr = '';
|
|
120
|
-
for (const char of
|
|
160
|
+
for (const char of (saltHex + authCheck)) {
|
|
121
161
|
const val = parseInt(char, 16);
|
|
122
162
|
headerStr += HEADER_ALPHABET[val];
|
|
123
163
|
}
|
|
@@ -126,7 +166,6 @@ export class CipherEngine {
|
|
|
126
166
|
|
|
127
167
|
static decodeHeader(headerStr) {
|
|
128
168
|
let hexString = '';
|
|
129
|
-
// Use segmenter here just in case moon emojis have variation selectors
|
|
130
169
|
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
131
170
|
const segments = segmenter.segment(headerStr.trim());
|
|
132
171
|
|
|
@@ -135,119 +174,226 @@ export class CipherEngine {
|
|
|
135
174
|
if (index === -1) throw new Error("Invalid Header format.");
|
|
136
175
|
hexString += index.toString(16);
|
|
137
176
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
saltHex: hexString.substring(0, 32),
|
|
180
|
+
authCheckHex: hexString.length >= 40 ? hexString.substring(32, 40) : null
|
|
181
|
+
};
|
|
141
182
|
}
|
|
142
183
|
|
|
184
|
+
// --- STREAMING ENCRYPTION ---
|
|
185
|
+
|
|
143
186
|
getEncryptStream() {
|
|
144
187
|
if (!this.isReady) throw new Error("Engine not initialized");
|
|
145
188
|
const engine = this;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
189
|
+
let buffer = Buffer.alloc(0);
|
|
190
|
+
|
|
149
191
|
return new Transform({
|
|
150
192
|
transform(chunk, encoding, callback) {
|
|
151
|
-
|
|
193
|
+
const str = chunk.toString('utf8');
|
|
152
194
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
195
|
+
// --- FIXED REGEX LOGIC ---
|
|
196
|
+
// Separate alpha keywords (int, void) from symbols (#include)
|
|
197
|
+
const alphaKeywords = C_KEYWORDS.filter(k => /^\w+$/.test(k)).sort((a,b)=>b.length-a.length).join('|');
|
|
198
|
+
const symKeywords = C_KEYWORDS.filter(k => !/^\w+$/.test(k)).map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
156
199
|
|
|
157
|
-
|
|
158
|
-
|
|
200
|
+
// Match: \b(int|void)\b OR (#include)
|
|
201
|
+
// This ensures 'secretCode' isn't matched as 'Code' containing 'do' or 'int'
|
|
202
|
+
const regex = new RegExp(`(\\b(?:${alphaKeywords})\\b|(?:${symKeywords}))`, 'g');
|
|
159
203
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
204
|
+
const parts = str.split(regex);
|
|
205
|
+
|
|
206
|
+
for (const part of parts) {
|
|
207
|
+
if (!part) continue;
|
|
208
|
+
|
|
209
|
+
if (C_KEYWORDS.includes(part)) {
|
|
210
|
+
if (buffer.length > 0) {
|
|
211
|
+
this.push(engine._flushDataBuffer(buffer));
|
|
212
|
+
buffer = Buffer.alloc(0);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const baseIdx = engine.keywordEmojis.indexOf(engine.keywordMap.get(part));
|
|
216
|
+
const shift = Number(engine.rng.next() % BigInt(engine.keywordEmojis.length));
|
|
217
|
+
const newIdx = (baseIdx + shift) % engine.keywordEmojis.length;
|
|
218
|
+
const polyEmoji = engine.keywordEmojis[newIdx];
|
|
219
|
+
|
|
220
|
+
const outBuf = Buffer.from(polyEmoji);
|
|
221
|
+
engine.hmac.update(outBuf);
|
|
222
|
+
this.push(engine._wrapOutput(outBuf));
|
|
223
|
+
|
|
164
224
|
} else {
|
|
165
|
-
|
|
225
|
+
buffer = Buffer.concat([buffer, Buffer.from(part, 'utf8')]);
|
|
226
|
+
|
|
227
|
+
while (buffer.length >= 5) {
|
|
228
|
+
const chunk = buffer.subarray(0, 5);
|
|
229
|
+
buffer = buffer.subarray(5);
|
|
230
|
+
|
|
231
|
+
const enc = engine._encodeBase1024(chunk);
|
|
232
|
+
engine.hmac.update(enc);
|
|
233
|
+
this.push(engine._wrapOutput(enc));
|
|
234
|
+
}
|
|
166
235
|
}
|
|
167
236
|
}
|
|
168
|
-
|
|
169
|
-
buffer = '';
|
|
170
|
-
this.push(output);
|
|
171
237
|
callback();
|
|
172
238
|
},
|
|
239
|
+
|
|
173
240
|
flush(callback) {
|
|
174
|
-
buffer
|
|
175
|
-
|
|
241
|
+
if (buffer.length > 0) {
|
|
242
|
+
const padded = Buffer.alloc(5);
|
|
243
|
+
buffer.copy(padded);
|
|
244
|
+
const enc = engine._encodeBase1024(padded);
|
|
245
|
+
engine.hmac.update(enc);
|
|
246
|
+
this.push(engine._wrapOutput(enc));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const digest = engine.hmac.digest();
|
|
250
|
+
let footerStr = '';
|
|
251
|
+
for (const byte of digest) {
|
|
252
|
+
const hex = byte.toString(16).padStart(2, '0');
|
|
253
|
+
for (const char of hex) {
|
|
254
|
+
const val = parseInt(char, 16);
|
|
255
|
+
footerStr += HEADER_ALPHABET[val];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
this.push(Buffer.from('\n' + footerStr));
|
|
176
259
|
callback();
|
|
177
260
|
}
|
|
178
261
|
});
|
|
179
262
|
}
|
|
180
263
|
|
|
264
|
+
_wrapOutput(bufferChunk) {
|
|
265
|
+
this.lineLength += bufferChunk.length;
|
|
266
|
+
if (this.lineLength > 300) {
|
|
267
|
+
this.lineLength = 0;
|
|
268
|
+
return Buffer.concat([bufferChunk, Buffer.from('\n')]);
|
|
269
|
+
}
|
|
270
|
+
return bufferChunk;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
_encodeBase1024(buffer5) {
|
|
274
|
+
let val = 0n;
|
|
275
|
+
for (let i = 0; i < 5; i++) {
|
|
276
|
+
val += BigInt(buffer5[i]) * (256n ** BigInt(i));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let output = '';
|
|
280
|
+
for (let i = 0; i < 4; i++) {
|
|
281
|
+
const idx = Number(val % 1024n);
|
|
282
|
+
val = val / 1024n;
|
|
283
|
+
output += this.dataAlphabet[idx];
|
|
284
|
+
}
|
|
285
|
+
return Buffer.from(output);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_flushDataBuffer(buf) {
|
|
289
|
+
if (buf.length === 0) return Buffer.alloc(0);
|
|
290
|
+
const padded = Buffer.alloc(5);
|
|
291
|
+
buf.copy(padded);
|
|
292
|
+
const enc = this._encodeBase1024(padded);
|
|
293
|
+
this.hmac.update(enc);
|
|
294
|
+
return this._wrapOutput(enc);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// --- STREAMING DECRYPTION ---
|
|
298
|
+
|
|
181
299
|
getDecryptStream() {
|
|
182
300
|
if (!this.isReady) throw new Error("Engine not initialized");
|
|
183
301
|
const engine = this;
|
|
184
|
-
const decoder = new StringDecoder('utf8');
|
|
185
302
|
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
186
303
|
|
|
187
|
-
|
|
304
|
+
const FOOTER_LEN = 64;
|
|
305
|
+
let emojiBuffer = [];
|
|
188
306
|
|
|
189
307
|
return new Transform({
|
|
190
308
|
transform(chunk, encoding, callback) {
|
|
191
|
-
|
|
192
|
-
|
|
309
|
+
const str = chunk.toString('utf8');
|
|
310
|
+
const segments = [...segmenter.segment(str)];
|
|
193
311
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// 3. Process all BUT the last segment
|
|
198
|
-
// We keep the last segment in the buffer because it might be
|
|
199
|
-
// the start of a multi-codepoint emoji that was split by the chunk boundary.
|
|
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
|
-
}
|
|
312
|
+
for (const { segment } of segments) {
|
|
313
|
+
if (segment.match(/\s/)) continue; // Skip wraps
|
|
213
314
|
|
|
214
|
-
|
|
215
|
-
for (let i = 0; i < processUntilIndex; i++) {
|
|
216
|
-
const char = segments[i].segment;
|
|
217
|
-
processedString += char;
|
|
315
|
+
emojiBuffer.push(segment);
|
|
218
316
|
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
317
|
+
if (emojiBuffer.length > FOOTER_LEN) {
|
|
318
|
+
const emo = emojiBuffer.shift();
|
|
319
|
+
engine._processDecryptToken(emo, this);
|
|
320
|
+
engine.hmac.update(Buffer.from(emo));
|
|
223
321
|
}
|
|
224
322
|
}
|
|
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
323
|
callback();
|
|
236
324
|
},
|
|
325
|
+
|
|
237
326
|
flush(callback) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
327
|
+
if (emojiBuffer.length !== FOOTER_LEN) {
|
|
328
|
+
this.emit('error', new Error("File corrupted or truncated (No Footer)"));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const footerStr = emojiBuffer.join('');
|
|
333
|
+
const calcDigest = engine.hmac.digest('hex');
|
|
334
|
+
|
|
335
|
+
let footerHex = '';
|
|
336
|
+
try {
|
|
337
|
+
for (const char of footerStr) {
|
|
338
|
+
const idx = HEADER_ALPHABET.indexOf(char);
|
|
339
|
+
if (idx === -1) throw new Error();
|
|
340
|
+
footerHex += idx.toString(16);
|
|
246
341
|
}
|
|
247
|
-
|
|
342
|
+
} catch (e) {
|
|
343
|
+
this.emit('error', new Error("Invalid Integrity Seal"));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (footerHex !== calcDigest) {
|
|
348
|
+
this.emit('error', new Error("FILE_TAMPERED"));
|
|
349
|
+
return;
|
|
248
350
|
}
|
|
249
351
|
callback();
|
|
250
352
|
}
|
|
251
353
|
});
|
|
252
354
|
}
|
|
355
|
+
|
|
356
|
+
decodeDataBuf = [];
|
|
357
|
+
|
|
358
|
+
_processDecryptToken(emo, stream) {
|
|
359
|
+
if (this.keywordReverseMap.has(emo)) {
|
|
360
|
+
const currentR = Number(this.rng.next() % BigInt(this.keywordEmojis.length));
|
|
361
|
+
const emoIdx = this.keywordEmojis.indexOf(emo);
|
|
362
|
+
|
|
363
|
+
let baseIdx = (emoIdx - currentR) % this.keywordEmojis.length;
|
|
364
|
+
if (baseIdx < 0) baseIdx += this.keywordEmojis.length;
|
|
365
|
+
|
|
366
|
+
const originalEmo = this.keywordEmojis[baseIdx];
|
|
367
|
+
const keyword = this.keywordReverseMap.get(originalEmo);
|
|
368
|
+
|
|
369
|
+
if (this.decodeDataBuf.length > 0) {
|
|
370
|
+
this.decodeDataBuf = [];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
stream.push(keyword);
|
|
374
|
+
|
|
375
|
+
} else if (this.dataReverseMap.has(emo)) {
|
|
376
|
+
this.decodeDataBuf.push(this.dataReverseMap.get(emo));
|
|
377
|
+
if (this.decodeDataBuf.length === 4) {
|
|
378
|
+
const chunk = this._decodeBase1024(this.decodeDataBuf);
|
|
379
|
+
this.decodeDataBuf = [];
|
|
380
|
+
const cleanChunk = chunk.filter(b => b !== 0x00);
|
|
381
|
+
stream.push(cleanChunk);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
_decodeBase1024(indices) {
|
|
387
|
+
let val = 0n;
|
|
388
|
+
for (let i = 3; i >= 0; i--) {
|
|
389
|
+
val = (val * 1024n) + BigInt(indices[i]);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const buf = Buffer.alloc(5);
|
|
393
|
+
for (let i = 0; i < 5; i++) {
|
|
394
|
+
buf[i] = Number(val % 256n);
|
|
395
|
+
val = val / 256n;
|
|
396
|
+
}
|
|
397
|
+
return buf;
|
|
398
|
+
}
|
|
253
399
|
}
|
package/package.json
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mojic",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Obfuscate C source code into
|
|
3
|
+
"version": "1.2.4",
|
|
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",
|
|
15
16
|
"url": "git+https://github.com/notamitgamer/mojic.git"
|
|
16
17
|
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"registry": "https://registry.npmjs.org/",
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
17
22
|
"dependencies": {
|
|
18
23
|
"chalk": "^5.3.0",
|
|
19
24
|
"commander": "^11.1.0",
|
|
20
25
|
"inquirer": "^9.2.12"
|
|
21
26
|
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"pkg": "^5.8.1"
|
|
29
|
+
},
|
|
22
30
|
"engines": {
|
|
23
31
|
"node": ">=18.0.0"
|
|
24
32
|
},
|
|
@@ -28,8 +36,22 @@
|
|
|
28
36
|
"emoji",
|
|
29
37
|
"obfuscation",
|
|
30
38
|
"c",
|
|
31
|
-
"security"
|
|
39
|
+
"security",
|
|
40
|
+
"polymorphic"
|
|
32
41
|
],
|
|
33
42
|
"author": "notamitgamer",
|
|
34
|
-
"license": "Apache-2.0"
|
|
35
|
-
|
|
43
|
+
"license": "Apache-2.0",
|
|
44
|
+
"pkg": {
|
|
45
|
+
"scripts": "bin/mojic.js",
|
|
46
|
+
"assets": [
|
|
47
|
+
"lib/**/*",
|
|
48
|
+
"package.json"
|
|
49
|
+
],
|
|
50
|
+
"targets": [
|
|
51
|
+
"node18-win-x64",
|
|
52
|
+
"node18-linux-x64",
|
|
53
|
+
"node18-macos-x64"
|
|
54
|
+
],
|
|
55
|
+
"outputPath": "dist"
|
|
56
|
+
}
|
|
57
|
+
}
|