stegdoc 1.0.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.
package/src/index.js ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const chalk = require('chalk');
5
+ const encodeCommand = require('./commands/encode');
6
+ const decodeCommand = require('./commands/decode');
7
+ const infoCommand = require('./commands/info');
8
+ const verifyCommand = require('./commands/verify');
9
+
10
+ // CLI Configuration
11
+ program
12
+ .name('stegdoc')
13
+ .description('Hide files inside Office documents with AES-256 encryption and steganography')
14
+ .version('1.0.0');
15
+
16
+ // Encode command
17
+ program
18
+ .command('encode <file>')
19
+ .description('Encode a file into XLSX/DOCX format with compression and optional encryption')
20
+ .option('-o, --output-dir <dir>', 'Output directory for files', process.cwd())
21
+ .option('-s, --chunk-size <size>', 'Maximum size per output file (e.g., "5MB", "25MB")', '5MB')
22
+ .option('-f, --format <format>', 'Output format: xlsx (default) or docx', 'xlsx')
23
+ .option('-p, --password <password>', 'Encryption password (optional, but recommended)')
24
+ .option('--force', 'Overwrite existing files without asking')
25
+ .option('-q, --quiet', 'Minimal output (for scripting)')
26
+ .option('-y, --yes', 'Skip interactive prompts, use defaults')
27
+ .action(async (file, options) => {
28
+ try {
29
+ await encodeCommand(file, options);
30
+ } catch (error) {
31
+ console.error(chalk.red(`Error: ${error.message}`));
32
+ process.exit(1);
33
+ }
34
+ });
35
+
36
+ // Decode command
37
+ program
38
+ .command('decode <file>')
39
+ .description('Decode and decrypt an XLSX/DOCX file back to original format')
40
+ .option('-o, --output <path>', 'Output file path (defaults to original filename)')
41
+ .option('-p, --password <password>', 'Decryption password (required for encrypted files)')
42
+ .option('--force', 'Overwrite existing files without asking')
43
+ .option('-q, --quiet', 'Minimal output (for scripting)')
44
+ .option('-y, --yes', 'Skip interactive prompts, fail if password needed')
45
+ .action(async (file, options) => {
46
+ try {
47
+ await decodeCommand(file, options);
48
+ } catch (error) {
49
+ console.error(chalk.red(`Error: ${error.message}`));
50
+ process.exit(1);
51
+ }
52
+ });
53
+
54
+ // Info command
55
+ program
56
+ .command('info <file>')
57
+ .description('Show information about an encoded file without decoding')
58
+ .action(async (file, options) => {
59
+ try {
60
+ await infoCommand(file, options);
61
+ } catch (error) {
62
+ console.error(chalk.red(`Error: ${error.message}`));
63
+ process.exit(1);
64
+ }
65
+ });
66
+
67
+ // Verify command
68
+ program
69
+ .command('verify <file>')
70
+ .description('Verify that a file can be decoded without actually decoding')
71
+ .option('-p, --password <password>', 'Password to verify (optional)')
72
+ .action(async (file, options) => {
73
+ try {
74
+ await verifyCommand(file, options);
75
+ } catch (error) {
76
+ console.error(chalk.red(`Error: ${error.message}`));
77
+ process.exit(1);
78
+ }
79
+ });
80
+
81
+ // Show help by default if no command is provided
82
+ if (!process.argv.slice(2).length) {
83
+ program.outputHelp();
84
+ }
85
+
86
+ // Parse arguments
87
+ program.parse(process.argv);
@@ -0,0 +1,97 @@
1
+ const zlib = require('zlib');
2
+
3
+ /**
4
+ * MIME types that are already compressed - no benefit from additional compression
5
+ */
6
+ const COMPRESSED_MIMES = new Set([
7
+ // Archives
8
+ 'application/zip',
9
+ 'application/x-7z-compressed',
10
+ 'application/x-rar-compressed',
11
+ 'application/gzip',
12
+ 'application/x-gzip',
13
+ 'application/x-bzip2',
14
+ 'application/x-xz',
15
+ 'application/x-tar',
16
+ 'application/x-lzip',
17
+ 'application/x-lzma',
18
+ 'application/zstd',
19
+
20
+ // Images (lossy compressed)
21
+ 'image/jpeg',
22
+ 'image/png',
23
+ 'image/gif',
24
+ 'image/webp',
25
+ 'image/avif',
26
+ 'image/heic',
27
+ 'image/heif',
28
+ 'image/jxl',
29
+
30
+ // Audio
31
+ 'audio/mpeg', // mp3
32
+ 'audio/ogg',
33
+ 'audio/flac',
34
+ 'audio/aac',
35
+ 'audio/mp4',
36
+ 'audio/x-m4a',
37
+ 'audio/opus',
38
+
39
+ // Video
40
+ 'video/mp4',
41
+ 'video/webm',
42
+ 'video/x-matroska', // mkv
43
+ 'video/quicktime', // mov
44
+ 'video/x-msvideo', // avi
45
+ 'video/mpeg',
46
+
47
+ // Documents (OOXML are zip-based)
48
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // docx
49
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // xlsx
50
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // pptx
51
+ 'application/pdf',
52
+ 'application/epub+zip',
53
+ ]);
54
+
55
+ /**
56
+ * Check if a file type is already compressed
57
+ * @param {string|null} mime - MIME type from file-type detection
58
+ * @returns {boolean}
59
+ */
60
+ function isCompressedMime(mime) {
61
+ return mime ? COMPRESSED_MIMES.has(mime) : false;
62
+ }
63
+
64
+ /**
65
+ * Compress data using gzip
66
+ * @param {Buffer} buffer - Data to compress
67
+ * @returns {Promise<Buffer>} Compressed data
68
+ */
69
+ function compress(buffer) {
70
+ return new Promise((resolve, reject) => {
71
+ zlib.gzip(buffer, { level: 9 }, (err, result) => {
72
+ if (err) reject(err);
73
+ else resolve(result);
74
+ });
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Decompress gzip data
80
+ * @param {Buffer} buffer - Compressed data
81
+ * @returns {Promise<Buffer>} Decompressed data
82
+ */
83
+ function decompress(buffer) {
84
+ return new Promise((resolve, reject) => {
85
+ zlib.gunzip(buffer, (err, result) => {
86
+ if (err) reject(err);
87
+ else resolve(result);
88
+ });
89
+ });
90
+ }
91
+
92
+ module.exports = {
93
+ isCompressedMime,
94
+ compress,
95
+ decompress,
96
+ COMPRESSED_MIMES,
97
+ };
@@ -0,0 +1,118 @@
1
+ const crypto = require('crypto');
2
+
3
+ // AES-256-GCM Configuration
4
+ const ALGORITHM = 'aes-256-gcm';
5
+ const KEY_LENGTH = 32; // 256 bits
6
+ const IV_LENGTH = 12; // 96 bits (recommended for GCM)
7
+ const SALT_LENGTH = 16; // 128 bits
8
+ const AUTH_TAG_LENGTH = 16; // 128 bits
9
+ const PBKDF2_ITERATIONS = 100000;
10
+
11
+ /**
12
+ * Derive a key from password using PBKDF2
13
+ * @param {string} password - User password
14
+ * @param {Buffer} salt - Salt for key derivation
15
+ * @returns {Buffer} Derived key
16
+ */
17
+ function deriveKey(password, salt) {
18
+ return crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha256');
19
+ }
20
+
21
+ /**
22
+ * Encrypt plaintext using AES-256-GCM
23
+ * @param {string} plaintext - Data to encrypt
24
+ * @param {string} password - User password
25
+ * @returns {object} { ciphertext, iv, salt, authTag } - all as base64 strings
26
+ */
27
+ function encrypt(plaintext, password) {
28
+ // Generate random salt and IV
29
+ const salt = crypto.randomBytes(SALT_LENGTH);
30
+ const iv = crypto.randomBytes(IV_LENGTH);
31
+
32
+ // Derive key from password
33
+ const key = deriveKey(password, salt);
34
+
35
+ // Create cipher and encrypt
36
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv, {
37
+ authTagLength: AUTH_TAG_LENGTH,
38
+ });
39
+
40
+ let ciphertext = cipher.update(plaintext, 'utf8', 'base64');
41
+ ciphertext += cipher.final('base64');
42
+
43
+ // Get authentication tag
44
+ const authTag = cipher.getAuthTag();
45
+
46
+ return {
47
+ ciphertext,
48
+ iv: iv.toString('base64'),
49
+ salt: salt.toString('base64'),
50
+ authTag: authTag.toString('base64'),
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Decrypt ciphertext using AES-256-GCM
56
+ * @param {string} ciphertext - Encrypted data (base64)
57
+ * @param {string} password - User password
58
+ * @param {string} ivBase64 - Initialization vector (base64)
59
+ * @param {string} saltBase64 - Salt used for key derivation (base64)
60
+ * @param {string} authTagBase64 - Authentication tag (base64)
61
+ * @returns {string} Decrypted plaintext
62
+ * @throws {Error} If decryption fails (wrong password or tampered data)
63
+ */
64
+ function decrypt(ciphertext, password, ivBase64, saltBase64, authTagBase64) {
65
+ // Convert from base64
66
+ const iv = Buffer.from(ivBase64, 'base64');
67
+ const salt = Buffer.from(saltBase64, 'base64');
68
+ const authTag = Buffer.from(authTagBase64, 'base64');
69
+
70
+ // Derive key from password
71
+ const key = deriveKey(password, salt);
72
+
73
+ // Create decipher
74
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, {
75
+ authTagLength: AUTH_TAG_LENGTH,
76
+ });
77
+
78
+ // Set auth tag for verification
79
+ decipher.setAuthTag(authTag);
80
+
81
+ try {
82
+ let plaintext = decipher.update(ciphertext, 'base64', 'utf8');
83
+ plaintext += decipher.final('utf8');
84
+ return plaintext;
85
+ } catch (error) {
86
+ throw new Error('Decryption failed: Invalid password or corrupted data');
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Pack encryption metadata into a single string for storage
92
+ * @param {object} encryptionData - { iv, salt, authTag }
93
+ * @returns {string} Packed string (iv:salt:authTag in base64)
94
+ */
95
+ function packEncryptionMeta(encryptionData) {
96
+ return `${encryptionData.iv}:${encryptionData.salt}:${encryptionData.authTag}`;
97
+ }
98
+
99
+ /**
100
+ * Unpack encryption metadata from storage string
101
+ * @param {string} packed - Packed string (iv:salt:authTag)
102
+ * @returns {object} { iv, salt, authTag }
103
+ */
104
+ function unpackEncryptionMeta(packed) {
105
+ const [iv, salt, authTag] = packed.split(':');
106
+ if (!iv || !salt || !authTag) {
107
+ throw new Error('Invalid encryption metadata format');
108
+ }
109
+ return { iv, salt, authTag };
110
+ }
111
+
112
+ module.exports = {
113
+ encrypt,
114
+ decrypt,
115
+ deriveKey,
116
+ packEncryptionMeta,
117
+ unpackEncryptionMeta,
118
+ };
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Decoy Data Generator - Server Metrics Report
3
+ * Generates realistic-looking server monitoring/metrics data
4
+ */
5
+
6
+ // Fixed list of servers (small dev team setup - ~10 servers total)
7
+ const SERVERS = [
8
+ 'app-prod-01',
9
+ 'app-prod-02',
10
+ 'api-prod-01',
11
+ 'db-prod-01',
12
+ 'redis-prod-01',
13
+ 'nginx-prod-01',
14
+ 'app-dev-01',
15
+ 'db-dev-01',
16
+ 'jenkins-01',
17
+ 'gitlab-01',
18
+ ];
19
+
20
+ const STATUS_VALUES = [
21
+ { status: 'healthy', weight: 85 },
22
+ { status: 'warning', weight: 10 },
23
+ { status: 'critical', weight: 3 },
24
+ { status: 'maintenance', weight: 2 },
25
+ ];
26
+
27
+ /**
28
+ * Generate a random float between min and max with specified decimal places
29
+ */
30
+ function randomFloat(min, max, decimals = 1) {
31
+ const val = Math.random() * (max - min) + min;
32
+ return parseFloat(val.toFixed(decimals));
33
+ }
34
+
35
+ /**
36
+ * Generate a random integer between min and max (inclusive)
37
+ */
38
+ function randomInt(min, max) {
39
+ return Math.floor(Math.random() * (max - min + 1)) + min;
40
+ }
41
+
42
+ /**
43
+ * Generate a random element from an array
44
+ */
45
+ function randomElement(arr) {
46
+ return arr[Math.floor(Math.random() * arr.length)];
47
+ }
48
+
49
+ /**
50
+ * Generate a weighted random status
51
+ */
52
+ function generateStatus() {
53
+ const totalWeight = STATUS_VALUES.reduce((sum, s) => sum + s.weight, 0);
54
+ let random = Math.random() * totalWeight;
55
+
56
+ for (const item of STATUS_VALUES) {
57
+ random -= item.weight;
58
+ if (random <= 0) {
59
+ return item.status;
60
+ }
61
+ }
62
+ return 'healthy';
63
+ }
64
+
65
+ /**
66
+ * Pick a server from the fixed list
67
+ */
68
+ function generateServerId() {
69
+ return randomElement(SERVERS);
70
+ }
71
+
72
+ // Time window configuration (last 4 hours of data)
73
+ const HOURS_WINDOW = 4;
74
+
75
+ // Shared time window for multi-part consistency
76
+ let sharedTimeWindow = null;
77
+
78
+ /**
79
+ * Initialize or get the shared time window
80
+ * This ensures multi-part files have continuous timestamps
81
+ */
82
+ function getTimeWindow() {
83
+ if (!sharedTimeWindow) {
84
+ const now = new Date();
85
+ sharedTimeWindow = {
86
+ end: now.getTime(),
87
+ start: now.getTime() - (HOURS_WINDOW * 60 * 60 * 1000),
88
+ };
89
+ }
90
+ return sharedTimeWindow;
91
+ }
92
+
93
+ /**
94
+ * Reset the time window (called between separate encode sessions)
95
+ */
96
+ function resetTimeWindow() {
97
+ sharedTimeWindow = null;
98
+ }
99
+
100
+ /**
101
+ * Generate a realistic timestamp within the configured time window
102
+ */
103
+ function generateTimestamp() {
104
+ const window = getTimeWindow();
105
+ const timestamp = new Date(window.start + Math.random() * (window.end - window.start));
106
+
107
+ // Format: YYYY-MM-DD HH:MM:SS
108
+ const year = timestamp.getFullYear();
109
+ const month = (timestamp.getMonth() + 1).toString().padStart(2, '0');
110
+ const day = timestamp.getDate().toString().padStart(2, '0');
111
+ const hours = timestamp.getHours().toString().padStart(2, '0');
112
+ const minutes = timestamp.getMinutes().toString().padStart(2, '0');
113
+ const seconds = timestamp.getSeconds().toString().padStart(2, '0');
114
+
115
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
116
+ }
117
+
118
+ /**
119
+ * Generate CPU usage based on status
120
+ */
121
+ function generateCpuUsage(status) {
122
+ switch (status) {
123
+ case 'critical':
124
+ return randomFloat(90, 99);
125
+ case 'warning':
126
+ return randomFloat(70, 89);
127
+ case 'maintenance':
128
+ return randomFloat(0, 10);
129
+ default:
130
+ return randomFloat(15, 69);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Generate memory usage based on status
136
+ */
137
+ function generateMemoryUsage(status) {
138
+ switch (status) {
139
+ case 'critical':
140
+ return randomFloat(92, 99);
141
+ case 'warning':
142
+ return randomFloat(75, 91);
143
+ case 'maintenance':
144
+ return randomFloat(5, 20);
145
+ default:
146
+ return randomFloat(30, 74);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Generate disk usage (generally more stable than CPU/memory)
152
+ */
153
+ function generateDiskUsage(status) {
154
+ switch (status) {
155
+ case 'critical':
156
+ return randomFloat(95, 99);
157
+ case 'warning':
158
+ return randomFloat(80, 94);
159
+ default:
160
+ return randomFloat(25, 79);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Generate network I/O in MB/s
166
+ */
167
+ function generateNetworkIO() {
168
+ return randomFloat(0.5, 150, 2);
169
+ }
170
+
171
+ /**
172
+ * Generate request count (for web/api servers)
173
+ */
174
+ function generateRequestCount(serverId) {
175
+ // Higher for app/api/nginx servers
176
+ if (serverId.match(/^(app|api|nginx)/)) {
177
+ return randomInt(500, 15000);
178
+ }
179
+ // Low for CI/CD servers
180
+ if (serverId.match(/^(jenkins|gitlab)/)) {
181
+ return randomInt(10, 200);
182
+ }
183
+ return randomInt(50, 1000);
184
+ }
185
+
186
+ /**
187
+ * Generate response time in ms
188
+ */
189
+ function generateResponseTime(status) {
190
+ switch (status) {
191
+ case 'critical':
192
+ return randomInt(2000, 10000);
193
+ case 'warning':
194
+ return randomInt(500, 1999);
195
+ default:
196
+ return randomInt(5, 499);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Generate uptime in hours
202
+ */
203
+ function generateUptime(status) {
204
+ if (status === 'maintenance') {
205
+ return randomFloat(0, 2, 1);
206
+ }
207
+ // Servers typically have long uptimes
208
+ return randomFloat(24, 2160, 1); // Up to 90 days
209
+ }
210
+
211
+ /**
212
+ * Generate column headers
213
+ */
214
+ function generateDecoyHeaders() {
215
+ return [
216
+ 'Timestamp',
217
+ 'Server ID',
218
+ 'Status',
219
+ 'CPU %',
220
+ 'Memory %',
221
+ 'Disk %',
222
+ 'Network (MB/s)',
223
+ 'Requests',
224
+ 'Resp Time (ms)',
225
+ 'Uptime (hrs)',
226
+ ];
227
+ }
228
+
229
+ /**
230
+ * Generate a single row of server metrics
231
+ */
232
+ function generateMetricsRow() {
233
+ const serverId = generateServerId();
234
+ const status = generateStatus();
235
+
236
+ return {
237
+ timestamp: generateTimestamp(),
238
+ serverId,
239
+ status,
240
+ cpu: generateCpuUsage(status),
241
+ memory: generateMemoryUsage(status),
242
+ disk: generateDiskUsage(status),
243
+ network: generateNetworkIO(),
244
+ requests: generateRequestCount(serverId),
245
+ responseTime: generateResponseTime(status),
246
+ uptime: generateUptime(status),
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Generate decoy data with specified number of rows
252
+ * @param {number} rowCount - Number of rows to generate (default 100)
253
+ * @returns {Array<object>} Array of metrics row objects
254
+ */
255
+ function generateDecoyData(rowCount = 100) {
256
+ const rows = [];
257
+ for (let i = 0; i < rowCount; i++) {
258
+ rows.push(generateMetricsRow());
259
+ }
260
+ // Sort by timestamp descending (most recent first)
261
+ rows.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
262
+ return rows;
263
+ }
264
+
265
+ /**
266
+ * Calculate appropriate row count based on payload size
267
+ * @param {number} payloadSizeBytes - Size of encrypted payload
268
+ * @returns {number} Recommended number of decoy rows
269
+ */
270
+ function calculateDecoyRowCount(payloadSizeBytes) {
271
+ const estimatedBytesPerRow = 150;
272
+ const targetDecoyRatio = 0.4;
273
+
274
+ const minRows = 50;
275
+ const maxRows = 10000;
276
+
277
+ const calculatedRows = Math.floor((payloadSizeBytes * targetDecoyRatio) / estimatedBytesPerRow);
278
+
279
+ return Math.max(minRows, Math.min(maxRows, calculatedRows));
280
+ }
281
+
282
+ /**
283
+ * Convert row object to array of values (for Excel)
284
+ */
285
+ function rowToArray(row) {
286
+ return [
287
+ row.timestamp,
288
+ row.serverId,
289
+ row.status,
290
+ row.cpu,
291
+ row.memory,
292
+ row.disk,
293
+ row.network,
294
+ row.requests,
295
+ row.responseTime,
296
+ row.uptime,
297
+ ];
298
+ }
299
+
300
+ module.exports = {
301
+ generateDecoyHeaders,
302
+ generateDecoyData,
303
+ calculateDecoyRowCount,
304
+ rowToArray,
305
+ resetTimeWindow,
306
+ };