stegdoc 5.7.0 → 6.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.
- package/README.md +240 -106
- package/package.json +27 -5
- package/src/commands/decode.js +78 -436
- package/src/commands/encode.js +214 -137
- package/src/commands/info.js +65 -94
- package/src/commands/verify.js +76 -176
- package/src/index.js +9 -6
- package/src/lib/bindings.js +99 -0
- package/src/lib/compression.js +1 -161
- package/src/lib/crypto.js +0 -107
- package/src/lib/docx-handler.js +2 -278
- package/src/lib/log-generator.js +5 -2
- package/src/lib/metadata.js +7 -88
- package/src/lib/native.js +68 -0
- package/src/lib/streams.js +0 -139
- package/src/lib/utils.js +0 -27
- package/src/lib/xlsx-handler.js +1 -272
- package/src/lib/xlsx-writer.js +10 -0
- package/src/lib/file-handler.js +0 -113
- package/src/lib/file-utils.js +0 -160
- package/src/lib/xml-utils.js +0 -115
package/src/lib/crypto.js
CHANGED
|
@@ -18,75 +18,6 @@ function deriveKey(password, salt) {
|
|
|
18
18
|
return crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha256');
|
|
19
19
|
}
|
|
20
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
21
|
/**
|
|
91
22
|
* Pack encryption metadata into a single string for storage
|
|
92
23
|
* @param {object} encryptionData - { iv, salt, authTag }
|
|
@@ -96,19 +27,6 @@ function packEncryptionMeta(encryptionData) {
|
|
|
96
27
|
return `${encryptionData.iv}:${encryptionData.salt}:${encryptionData.authTag}`;
|
|
97
28
|
}
|
|
98
29
|
|
|
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
30
|
/**
|
|
113
31
|
* Generate a random salt for key derivation (one per encode session)
|
|
114
32
|
* @returns {Buffer} Random salt
|
|
@@ -139,34 +57,9 @@ function createEncryptStream(password, salt) {
|
|
|
139
57
|
};
|
|
140
58
|
}
|
|
141
59
|
|
|
142
|
-
/**
|
|
143
|
-
* Create a streaming decrypt decipher for per-part decryption.
|
|
144
|
-
* Auth tag is set immediately and verified on .final().
|
|
145
|
-
* @param {string} password - User password
|
|
146
|
-
* @param {string} ivBase64 - IV (base64)
|
|
147
|
-
* @param {string} saltBase64 - Salt (base64)
|
|
148
|
-
* @param {string} authTagBase64 - Auth tag (base64)
|
|
149
|
-
* @returns {crypto.Decipher} Decipher transform stream
|
|
150
|
-
*/
|
|
151
|
-
function createDecryptStream(password, ivBase64, saltBase64, authTagBase64) {
|
|
152
|
-
const iv = Buffer.from(ivBase64, 'base64');
|
|
153
|
-
const salt = Buffer.from(saltBase64, 'base64');
|
|
154
|
-
const authTag = Buffer.from(authTagBase64, 'base64');
|
|
155
|
-
const key = deriveKey(password, salt);
|
|
156
|
-
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, {
|
|
157
|
-
authTagLength: AUTH_TAG_LENGTH,
|
|
158
|
-
});
|
|
159
|
-
decipher.setAuthTag(authTag);
|
|
160
|
-
return decipher;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
60
|
module.exports = {
|
|
164
|
-
encrypt,
|
|
165
|
-
decrypt,
|
|
166
61
|
deriveKey,
|
|
167
62
|
packEncryptionMeta,
|
|
168
|
-
unpackEncryptionMeta,
|
|
169
63
|
generateSalt,
|
|
170
64
|
createEncryptStream,
|
|
171
|
-
createDecryptStream,
|
|
172
65
|
};
|
package/src/lib/docx-handler.js
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
const {
|
|
2
2
|
Document, Paragraph, TextRun, Packer, Table, TableRow, TableCell,
|
|
3
|
-
AlignmentType, HeadingLevel, WidthType, ShadingType,
|
|
3
|
+
AlignmentType, HeadingLevel, WidthType, ShadingType,
|
|
4
4
|
} = require('docx');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { serializeMetadata, parseMetadata } = require('./metadata');
|
|
8
|
-
const { parseXmlFromZip, ensureArray, extractTextContent } = require('./xml-utils');
|
|
9
7
|
const { generateIncident, generateHebrewDate } = require('./docx-templates');
|
|
10
|
-
const {
|
|
11
|
-
encodePayloadToLogLines, decodeLogLines, generateLogHeaders, resetTimeState,
|
|
12
|
-
BYTES_PER_DATA_LINE, calculateDataLineCount,
|
|
13
|
-
} = require('./log-generator');
|
|
8
|
+
const { encodePayloadToLogLines, resetTimeState } = require('./log-generator');
|
|
14
9
|
|
|
15
10
|
// ─── Shared Styles ──────────────────────────────────────────────────────────
|
|
16
11
|
|
|
@@ -311,277 +306,6 @@ function createHebrewCell(text, isHeader, widthPct) {
|
|
|
311
306
|
});
|
|
312
307
|
}
|
|
313
308
|
|
|
314
|
-
// ─── v5 DOCX Reader ─────────────────────────────────────────────────────────
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Read a v5 log-embed DOCX file and extract payload.
|
|
318
|
-
* Scans for monospace paragraphs that contain log lines.
|
|
319
|
-
* @param {string} docxPath - Path to DOCX file
|
|
320
|
-
* @returns {object} { payloadBuffer, metadataJson, encryptionMeta, metadata, formatVersion }
|
|
321
|
-
*/
|
|
322
|
-
async function readDocxV5(docxPath) {
|
|
323
|
-
const docParsed = parseXmlFromZip(docxPath, 'word/document.xml');
|
|
324
|
-
if (!docParsed) {
|
|
325
|
-
throw new Error('Could not find document.xml in DOCX file');
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Also parse styles to identify monospace/code runs
|
|
329
|
-
const stylesParsed = parseXmlFromZip(docxPath, 'word/styles.xml');
|
|
330
|
-
|
|
331
|
-
// Extract all paragraphs with their formatting info
|
|
332
|
-
const body = docParsed?.document?.body;
|
|
333
|
-
if (!body) throw new Error('Empty document body');
|
|
334
|
-
|
|
335
|
-
const paragraphs = ensureArray(body.p);
|
|
336
|
-
const logLines = [];
|
|
337
|
-
|
|
338
|
-
for (const para of paragraphs) {
|
|
339
|
-
const runs = ensureArray(para.r);
|
|
340
|
-
let paraText = '';
|
|
341
|
-
let isCode = false;
|
|
342
|
-
|
|
343
|
-
for (const run of runs) {
|
|
344
|
-
if (run.t !== undefined) {
|
|
345
|
-
paraText += extractTextContent(run.t);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Check if run uses monospace font (Consolas/Courier)
|
|
349
|
-
const rPr = run.rPr;
|
|
350
|
-
if (rPr) {
|
|
351
|
-
const fonts = rPr.rFonts;
|
|
352
|
-
if (fonts) {
|
|
353
|
-
const fontName = fonts['@_w:ascii'] || fonts['@_ascii'] || '';
|
|
354
|
-
if (/consolas|courier/i.test(fontName)) {
|
|
355
|
-
isCode = true;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Also check paragraph-level shading as indicator of code block
|
|
362
|
-
const pPr = para.pPr;
|
|
363
|
-
if (pPr && pPr.shd) {
|
|
364
|
-
const fill = pPr.shd['@_w:fill'] || pPr.shd['@_fill'] || '';
|
|
365
|
-
if (fill === 'F2F2F2' || fill === 'f2f2f2') {
|
|
366
|
-
isCode = true;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Collect code paragraphs that look like log lines
|
|
371
|
-
if (isCode && paraText.trim().length > 0) {
|
|
372
|
-
// Skip the comment line
|
|
373
|
-
if (paraText.startsWith('//')) continue;
|
|
374
|
-
logLines.push(paraText.trim());
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (logLines.length === 0) {
|
|
379
|
-
throw new Error('No log lines found in DOCX file. This may not be a v5 stegdoc file.');
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Parse log lines back into row arrays
|
|
383
|
-
const rows = logLines.map(parseLogLine);
|
|
384
|
-
|
|
385
|
-
// Decode using the same engine as XLSX
|
|
386
|
-
return decodeLogLines(rows);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Parse a formatted log line string back into a row array.
|
|
391
|
-
* Input format: `IP - - [timestamp] "request" status bytes "referer" "ua" "requestId" "traceId"`
|
|
392
|
-
*/
|
|
393
|
-
function parseLogLine(line) {
|
|
394
|
-
// Regex to parse nginx combined log format with extra fields
|
|
395
|
-
const regex = /^(\S+)\s+-\s+-\s+(\[[^\]]+\])\s+"([^"]+)"\s+(\d+)\s+(\d+)\s+"([^"]+)"\s+"([^"]+)"\s+"([^"]+)"\s+"([^"]+)"$/;
|
|
396
|
-
const match = line.match(regex);
|
|
397
|
-
|
|
398
|
-
if (!match) {
|
|
399
|
-
// Fallback: try to extract what we can
|
|
400
|
-
return ['', '', '', line, '', '', '', '', '', ''];
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const [, ip, timestamp, request, status, bytes, referer, ua, requestId, traceId] = match;
|
|
404
|
-
|
|
405
|
-
// Extract method from request
|
|
406
|
-
const methodMatch = request.match(/^(\w+)\s/);
|
|
407
|
-
const method = methodMatch ? methodMatch[1] : '';
|
|
408
|
-
|
|
409
|
-
return [ip, timestamp, method, request, status, bytes, referer, ua, requestId, traceId];
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Detect if a DOCX file is v5 (log-embed) format.
|
|
414
|
-
* Checks for the presence of log-formatted monospace content.
|
|
415
|
-
*/
|
|
416
|
-
function detectDocxVersion(docxPath) {
|
|
417
|
-
try {
|
|
418
|
-
const docParsed = parseXmlFromZip(docxPath, 'word/document.xml');
|
|
419
|
-
if (!docParsed) return 'legacy';
|
|
420
|
-
|
|
421
|
-
const body = docParsed?.document?.body;
|
|
422
|
-
if (!body) return 'legacy';
|
|
423
|
-
|
|
424
|
-
// Quick check: look for the STGD05 marker in raw text
|
|
425
|
-
const paragraphs = ensureArray(body.p);
|
|
426
|
-
for (const para of paragraphs) {
|
|
427
|
-
const runs = ensureArray(para.r);
|
|
428
|
-
for (const run of runs) {
|
|
429
|
-
const text = extractTextContent(run.t || '');
|
|
430
|
-
if (text.includes('/api/v1/health/')) {
|
|
431
|
-
return 'v5';
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Check for WHITENER_METADATA (legacy)
|
|
437
|
-
for (const para of paragraphs) {
|
|
438
|
-
const runs = ensureArray(para.r);
|
|
439
|
-
for (const run of runs) {
|
|
440
|
-
const text = extractTextContent(run.t || '');
|
|
441
|
-
if (text.includes('WHITENER_METADATA:')) {
|
|
442
|
-
return 'legacy';
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return 'legacy';
|
|
448
|
-
} catch {
|
|
449
|
-
return 'legacy';
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// ─── Legacy DOCX (v3/v4) ───────────────────────────────────────────────────
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Create a legacy DOCX file with base64 content (v3/v4 format)
|
|
457
|
-
*/
|
|
458
|
-
async function createDocxWithBase64(options) {
|
|
459
|
-
const { base64Content, metadata, outputPath } = options;
|
|
460
|
-
|
|
461
|
-
const metadataStr = serializeMetadata(metadata);
|
|
462
|
-
|
|
463
|
-
const doc = new Document({
|
|
464
|
-
sections: [{
|
|
465
|
-
properties: {},
|
|
466
|
-
children: [
|
|
467
|
-
new Paragraph({
|
|
468
|
-
children: [
|
|
469
|
-
new TextRun({
|
|
470
|
-
text: `WHITENER_METADATA:${metadataStr}`,
|
|
471
|
-
size: 1,
|
|
472
|
-
}),
|
|
473
|
-
],
|
|
474
|
-
}),
|
|
475
|
-
new Paragraph({
|
|
476
|
-
children: [
|
|
477
|
-
new TextRun({ text: '---', break: 1 }),
|
|
478
|
-
],
|
|
479
|
-
}),
|
|
480
|
-
new Paragraph({
|
|
481
|
-
children: [
|
|
482
|
-
new TextRun({
|
|
483
|
-
text: base64Content,
|
|
484
|
-
font: 'Courier New',
|
|
485
|
-
size: 16,
|
|
486
|
-
}),
|
|
487
|
-
],
|
|
488
|
-
}),
|
|
489
|
-
],
|
|
490
|
-
}],
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
const buffer = await Packer.toBuffer(doc);
|
|
494
|
-
|
|
495
|
-
const outputDir = path.dirname(outputPath);
|
|
496
|
-
if (!fs.existsSync(outputDir)) {
|
|
497
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
fs.writeFileSync(outputPath, buffer);
|
|
501
|
-
return outputPath;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// ─── Unified Reader ─────────────────────────────────────────────────────────
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* Read a DOCX file, auto-detecting v5 vs legacy format.
|
|
508
|
-
*/
|
|
509
|
-
async function readDocxBase64(docxPath) {
|
|
510
|
-
if (!fs.existsSync(docxPath)) {
|
|
511
|
-
throw new Error(`DOCX file not found: ${docxPath}`);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const version = detectDocxVersion(docxPath);
|
|
515
|
-
|
|
516
|
-
if (version === 'v5') {
|
|
517
|
-
const result = await readDocxV5(docxPath);
|
|
518
|
-
return {
|
|
519
|
-
...result,
|
|
520
|
-
formatVersion: 'v5',
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// Legacy path
|
|
525
|
-
try {
|
|
526
|
-
const docParsed = parseXmlFromZip(docxPath, 'word/document.xml');
|
|
527
|
-
if (!docParsed) {
|
|
528
|
-
throw new Error('Could not find document.xml in DOCX file');
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const fullText = extractAllText(docParsed);
|
|
532
|
-
const metadataMarker = 'WHITENER_METADATA:';
|
|
533
|
-
const metadataStart = fullText.indexOf(metadataMarker);
|
|
534
|
-
|
|
535
|
-
if (metadataStart === -1) {
|
|
536
|
-
throw new Error('No metadata found in DOCX file. This may not be a stegdoc-encoded file.');
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
const separatorIndex = fullText.indexOf('---', metadataStart);
|
|
540
|
-
if (separatorIndex === -1) {
|
|
541
|
-
throw new Error('Invalid file format: separator not found');
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const metadataStr = fullText.substring(metadataStart + metadataMarker.length, separatorIndex).trim();
|
|
545
|
-
const metadata = parseMetadata(metadataStr);
|
|
546
|
-
const base64Content = fullText.substring(separatorIndex + 3).trim();
|
|
547
|
-
|
|
548
|
-
return {
|
|
549
|
-
base64Content,
|
|
550
|
-
metadata,
|
|
551
|
-
formatVersion: 'legacy',
|
|
552
|
-
};
|
|
553
|
-
} catch (error) {
|
|
554
|
-
throw new Error(`Failed to read DOCX file: ${error.message}`);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
/**
|
|
559
|
-
* Extract all text content from parsed DOCX document (legacy)
|
|
560
|
-
*/
|
|
561
|
-
function extractAllText(docParsed) {
|
|
562
|
-
let fullText = '';
|
|
563
|
-
const body = docParsed?.document?.body;
|
|
564
|
-
if (!body) return fullText;
|
|
565
|
-
|
|
566
|
-
const paragraphs = ensureArray(body.p);
|
|
567
|
-
for (const para of paragraphs) {
|
|
568
|
-
const runs = ensureArray(para.r);
|
|
569
|
-
for (const run of runs) {
|
|
570
|
-
if (run.t !== undefined) {
|
|
571
|
-
fullText += extractTextContent(run.t);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
return fullText;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
309
|
module.exports = {
|
|
579
|
-
// v5
|
|
580
310
|
createDocxV5,
|
|
581
|
-
readDocxV5,
|
|
582
|
-
detectDocxVersion,
|
|
583
|
-
// Legacy
|
|
584
|
-
createDocxWithBase64,
|
|
585
|
-
// Unified
|
|
586
|
-
readDocxBase64,
|
|
587
311
|
};
|
package/src/lib/log-generator.js
CHANGED
|
@@ -657,9 +657,12 @@ function decodeLogLines(allRows) {
|
|
|
657
657
|
const headerStr = headerPayload.toString('utf8');
|
|
658
658
|
|
|
659
659
|
// Parse header: "STGD05|<metaLen>|<encLen>|{metadataJson}{encryptionMeta}"
|
|
660
|
-
const markerIdx = headerStr.
|
|
660
|
+
const markerIdx = headerStr.search(/STGD0[56]\|/);
|
|
661
661
|
if (markerIdx === -1) {
|
|
662
|
-
throw new Error('Invalid
|
|
662
|
+
throw new Error('Invalid log-embed format: magic marker not found. This may not be a stegdoc file.');
|
|
663
|
+
}
|
|
664
|
+
if (headerStr.startsWith('STGD06|', markerIdx)) {
|
|
665
|
+
throw new Error('This file uses the v6 format, which the JavaScript decoder cannot read. The native engine is required.');
|
|
663
666
|
}
|
|
664
667
|
|
|
665
668
|
const afterMarker = headerStr.slice(markerIdx + 7);
|
package/src/lib/metadata.js
CHANGED
|
@@ -24,15 +24,11 @@ function createMetadata({
|
|
|
24
24
|
encrypted = true,
|
|
25
25
|
compressed = false,
|
|
26
26
|
contentHash = null,
|
|
27
|
-
// v5 fields
|
|
28
|
-
stegoMethod = null,
|
|
29
27
|
compressionAlgo = null,
|
|
30
28
|
payloadSize = null,
|
|
31
29
|
dataLineCount = null,
|
|
32
30
|
headerLineCount = null,
|
|
33
31
|
}) {
|
|
34
|
-
const isV5 = stegoMethod === 'log-embed';
|
|
35
|
-
|
|
36
32
|
const meta = {
|
|
37
33
|
originalFilename,
|
|
38
34
|
originalExtension,
|
|
@@ -44,20 +40,17 @@ function createMetadata({
|
|
|
44
40
|
encrypted,
|
|
45
41
|
compressed,
|
|
46
42
|
contentHash,
|
|
47
|
-
pipelineOrder:
|
|
43
|
+
pipelineOrder: 'brotli-encrypt-logEmbed',
|
|
48
44
|
encodingDate: new Date().toISOString(),
|
|
49
|
-
version:
|
|
45
|
+
version: '5.0.0',
|
|
50
46
|
tool: 'stegdoc',
|
|
47
|
+
stegoMethod: 'log-embed',
|
|
48
|
+
compressionAlgo: compressionAlgo || 'brotli',
|
|
51
49
|
};
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
meta.compressionAlgo = compressionAlgo || 'brotli';
|
|
57
|
-
if (payloadSize !== null) meta.payloadSize = payloadSize;
|
|
58
|
-
if (dataLineCount !== null) meta.dataLineCount = dataLineCount;
|
|
59
|
-
if (headerLineCount !== null) meta.headerLineCount = headerLineCount;
|
|
60
|
-
}
|
|
51
|
+
if (payloadSize !== null) meta.payloadSize = payloadSize;
|
|
52
|
+
if (dataLineCount !== null) meta.dataLineCount = dataLineCount;
|
|
53
|
+
if (headerLineCount !== null) meta.headerLineCount = headerLineCount;
|
|
61
54
|
|
|
62
55
|
return meta;
|
|
63
56
|
}
|
|
@@ -71,81 +64,7 @@ function serializeMetadata(metadata) {
|
|
|
71
64
|
return JSON.stringify(metadata);
|
|
72
65
|
}
|
|
73
66
|
|
|
74
|
-
/**
|
|
75
|
-
* Parse metadata from string
|
|
76
|
-
* @param {string} metadataStr - JSON string
|
|
77
|
-
* @returns {object} Metadata object
|
|
78
|
-
*/
|
|
79
|
-
function parseMetadata(metadataStr) {
|
|
80
|
-
try {
|
|
81
|
-
return JSON.parse(metadataStr);
|
|
82
|
-
} catch (error) {
|
|
83
|
-
throw new Error(`Failed to parse metadata: ${error.message}`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Validate metadata object
|
|
89
|
-
* @param {object} metadata - Metadata to validate
|
|
90
|
-
* @returns {boolean} True if valid
|
|
91
|
-
* @throws {Error} If metadata is invalid
|
|
92
|
-
*/
|
|
93
|
-
function validateMetadata(metadata) {
|
|
94
|
-
const required = ['originalFilename', 'originalExtension', 'hash', 'tool'];
|
|
95
|
-
|
|
96
|
-
for (const field of required) {
|
|
97
|
-
if (!metadata[field]) {
|
|
98
|
-
throw new Error(`Missing required metadata field: ${field}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (metadata.tool !== 'stegdoc' && metadata.tool !== 'whitener') {
|
|
103
|
-
throw new Error('Invalid tool identifier in metadata');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// If it's a multi-part file, validate part info
|
|
107
|
-
if (metadata.totalParts !== null && metadata.totalParts > 1) {
|
|
108
|
-
if (metadata.partNumber === null || metadata.partNumber < 1 || metadata.partNumber > metadata.totalParts) {
|
|
109
|
-
throw new Error('Invalid part number in metadata');
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Check if metadata indicates a multi-part file
|
|
118
|
-
* @param {object} metadata - Metadata object
|
|
119
|
-
* @returns {boolean} True if multi-part
|
|
120
|
-
*/
|
|
121
|
-
function isMultiPart(metadata) {
|
|
122
|
-
return metadata.totalParts !== null && metadata.totalParts > 1;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Check if metadata indicates the v4 streaming format
|
|
127
|
-
* @param {object} metadata - Metadata object
|
|
128
|
-
* @returns {boolean} True if streaming format (v4+)
|
|
129
|
-
*/
|
|
130
|
-
function isStreamingFormat(metadata) {
|
|
131
|
-
return metadata.pipelineOrder === 'compress-encrypt-base64';
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Check if metadata indicates the v5 log-embed format
|
|
136
|
-
* @param {object} metadata - Metadata object
|
|
137
|
-
* @returns {boolean} True if log-embed format (v5+)
|
|
138
|
-
*/
|
|
139
|
-
function isLogEmbedFormat(metadata) {
|
|
140
|
-
return metadata.stegoMethod === 'log-embed' || metadata.pipelineOrder === 'brotli-encrypt-logEmbed';
|
|
141
|
-
}
|
|
142
|
-
|
|
143
67
|
module.exports = {
|
|
144
68
|
createMetadata,
|
|
145
69
|
serializeMetadata,
|
|
146
|
-
parseMetadata,
|
|
147
|
-
validateMetadata,
|
|
148
|
-
isMultiPart,
|
|
149
|
-
isStreamingFormat,
|
|
150
|
-
isLogEmbedFormat,
|
|
151
70
|
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const { hostTarget, packageName } = require('./bindings');
|
|
6
|
+
|
|
7
|
+
const NATIVE_DIR = path.join(__dirname, '..', '..', 'native');
|
|
8
|
+
|
|
9
|
+
let binding;
|
|
10
|
+
let resolved = false;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load the native format engine, or return null when it is not available.
|
|
14
|
+
*
|
|
15
|
+
* Resolution order:
|
|
16
|
+
* 1. `STEGDOC_NATIVE_PATH`, an explicit `.node` file.
|
|
17
|
+
* 2. The platform package (`@stegdoc/binding-<suffix>`) from the published
|
|
18
|
+
* install's optionalDependencies.
|
|
19
|
+
* 3. `native/stegdoc.node`, the local `pnpm build:native` output.
|
|
20
|
+
*
|
|
21
|
+
* The binding is required for log-embed (v5/v6) decode and is the preferred
|
|
22
|
+
* encoder. The CommonJS v5 encoder survives only as a fallback for hosts where
|
|
23
|
+
* the binding cannot be loaded. Set `STEGDOC_DISABLE_NATIVE=1` to force that
|
|
24
|
+
* fallback; it does not affect decode, which has no JS path.
|
|
25
|
+
*
|
|
26
|
+
* @returns {object|null} The binding, or null when unavailable.
|
|
27
|
+
*/
|
|
28
|
+
function loadNative() {
|
|
29
|
+
if (resolved) return binding;
|
|
30
|
+
resolved = true;
|
|
31
|
+
|
|
32
|
+
if (process.env.STEGDOC_DISABLE_NATIVE === '1') {
|
|
33
|
+
binding = null;
|
|
34
|
+
return binding;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const explicit = process.env.STEGDOC_NATIVE_PATH;
|
|
38
|
+
if (explicit) {
|
|
39
|
+
binding = require(explicit);
|
|
40
|
+
return binding;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const target = hostTarget();
|
|
44
|
+
if (target) {
|
|
45
|
+
try {
|
|
46
|
+
binding = require(packageName(target));
|
|
47
|
+
return binding;
|
|
48
|
+
} catch {
|
|
49
|
+
// Not installed; fall through to the development build.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
binding = require(path.join(NATIVE_DIR, 'stegdoc.node'));
|
|
55
|
+
} catch {
|
|
56
|
+
binding = null;
|
|
57
|
+
}
|
|
58
|
+
return binding;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @returns {boolean} Whether the native engine is loaded.
|
|
63
|
+
*/
|
|
64
|
+
function nativeAvailable() {
|
|
65
|
+
return loadNative() !== null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { loadNative, nativeAvailable };
|