stegdoc 1.0.1 → 4.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/package.json +6 -7
- package/src/commands/decode.js +233 -91
- package/src/commands/encode.js +302 -199
- package/src/commands/info.js +10 -9
- package/src/commands/verify.js +65 -30
- package/src/index.js +2 -2
- package/src/lib/compression.js +18 -0
- package/src/lib/crypto.js +54 -0
- package/src/lib/docx-handler.js +1 -1
- package/src/lib/metadata.js +13 -2
- package/src/lib/streams.js +197 -0
- package/src/lib/utils.js +2 -2
- package/src/lib/xlsx-handler.js +133 -76
- package/bootstrap.js +0 -33
package/src/lib/xlsx-handler.js
CHANGED
|
@@ -11,7 +11,127 @@ const VISIBLE_SHEET_NAME = 'Server Metrics';
|
|
|
11
11
|
const CELL_CHUNK_SIZE = 32000; // Max characters per cell (~32KB)
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Create an XLSX file
|
|
14
|
+
* Create an XLSX file using streaming WorkbookWriter (v4 format).
|
|
15
|
+
* Memory-efficient: rows are freed after commit.
|
|
16
|
+
* @param {object} options
|
|
17
|
+
* @param {string} options.base64Content - Base64 content to store in this part
|
|
18
|
+
* @param {string} options.encryptionMeta - Packed encryption metadata (iv:salt:authTag) or ''
|
|
19
|
+
* @param {string} options.metadataJson - Serialized metadata JSON string
|
|
20
|
+
* @param {string} options.outputPath - Output file path
|
|
21
|
+
* @returns {Promise<string>} Path to created file
|
|
22
|
+
*/
|
|
23
|
+
async function createXlsxPartStreaming(options) {
|
|
24
|
+
const { base64Content, encryptionMeta, metadataJson, outputPath } = options;
|
|
25
|
+
|
|
26
|
+
// Ensure output directory exists
|
|
27
|
+
const outputDir = path.dirname(outputPath);
|
|
28
|
+
if (!fs.existsSync(outputDir)) {
|
|
29
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({
|
|
33
|
+
filename: outputPath,
|
|
34
|
+
useSharedStrings: false, // Inline strings for easier post-processing
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
workbook.creator = 'Microsoft Excel';
|
|
38
|
+
workbook.lastModifiedBy = 'Microsoft Excel';
|
|
39
|
+
workbook.created = new Date();
|
|
40
|
+
workbook.modified = new Date();
|
|
41
|
+
|
|
42
|
+
// === Sheet 1: Visible decoy data (server metrics) ===
|
|
43
|
+
const visibleSheet = workbook.addWorksheet(VISIBLE_SHEET_NAME, {
|
|
44
|
+
properties: { tabColor: { argb: '4472C4' } },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Set column widths before adding rows
|
|
48
|
+
visibleSheet.columns = [
|
|
49
|
+
{ width: 20 }, // Timestamp
|
|
50
|
+
{ width: 16 }, // Server ID
|
|
51
|
+
{ width: 12 }, // Status
|
|
52
|
+
{ width: 8 }, // CPU %
|
|
53
|
+
{ width: 10 }, // Memory %
|
|
54
|
+
{ width: 8 }, // Disk %
|
|
55
|
+
{ width: 14 }, // Network (MB/s)
|
|
56
|
+
{ width: 10 }, // Requests
|
|
57
|
+
{ width: 14 }, // Resp Time (ms)
|
|
58
|
+
{ width: 12 }, // Uptime (hrs)
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// Add headers
|
|
62
|
+
const headers = generateDecoyHeaders();
|
|
63
|
+
const headerRow = visibleSheet.addRow(headers);
|
|
64
|
+
|
|
65
|
+
const headerCount = headers.length;
|
|
66
|
+
for (let col = 1; col <= headerCount; col++) {
|
|
67
|
+
const cell = headerRow.getCell(col);
|
|
68
|
+
cell.font = { bold: true, size: 11, color: { argb: 'FFFFFF' } };
|
|
69
|
+
cell.fill = {
|
|
70
|
+
type: 'pattern',
|
|
71
|
+
pattern: 'solid',
|
|
72
|
+
fgColor: { argb: '2E7D32' },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
headerRow.commit();
|
|
76
|
+
|
|
77
|
+
// Generate and write decoy data rows, committing each to free memory
|
|
78
|
+
const payloadSize = base64Content.length;
|
|
79
|
+
const rowCount = calculateDecoyRowCount(payloadSize);
|
|
80
|
+
const decoyData = generateDecoyData(rowCount);
|
|
81
|
+
|
|
82
|
+
for (const row of decoyData) {
|
|
83
|
+
const dataRow = visibleSheet.addRow(rowToArray(row));
|
|
84
|
+
dataRow.commit();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Add filters
|
|
88
|
+
visibleSheet.autoFilter = {
|
|
89
|
+
from: 'A1',
|
|
90
|
+
to: `J${rowCount + 1}`,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
await visibleSheet.commit();
|
|
94
|
+
|
|
95
|
+
// === Sheet 2: Hidden payload (veryHidden) ===
|
|
96
|
+
const hiddenSheet = workbook.addWorksheet(HIDDEN_SHEET_NAME, {
|
|
97
|
+
state: 'veryHidden',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Split base64 content into cell-sized chunks
|
|
101
|
+
const chunks = splitIntoChunks(base64Content, CELL_CHUNK_SIZE);
|
|
102
|
+
|
|
103
|
+
// Row 1: metadata
|
|
104
|
+
const metaRow = hiddenSheet.getRow(1);
|
|
105
|
+
metaRow.getCell(1).value = encryptionMeta;
|
|
106
|
+
metaRow.getCell(2).value = metadataJson;
|
|
107
|
+
metaRow.getCell(3).value = chunks.length.toString();
|
|
108
|
+
metaRow.commit();
|
|
109
|
+
|
|
110
|
+
// Write cell chunks in rows starting from row 2, columns A-Z
|
|
111
|
+
const totalChunks = chunks.length;
|
|
112
|
+
const totalRows = Math.ceil(totalChunks / 26);
|
|
113
|
+
|
|
114
|
+
for (let rowIdx = 0; rowIdx < totalRows; rowIdx++) {
|
|
115
|
+
const sheetRow = hiddenSheet.getRow(rowIdx + 2);
|
|
116
|
+
const startChunk = rowIdx * 26;
|
|
117
|
+
const endChunk = Math.min(startChunk + 26, totalChunks);
|
|
118
|
+
|
|
119
|
+
for (let i = startChunk; i < endChunk; i++) {
|
|
120
|
+
const col = (i % 26) + 1;
|
|
121
|
+
sheetRow.getCell(col).value = chunks[i];
|
|
122
|
+
}
|
|
123
|
+
sheetRow.commit();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await hiddenSheet.commit();
|
|
127
|
+
await workbook.commit();
|
|
128
|
+
|
|
129
|
+
// WorkbookWriter natively supports veryHidden state — no post-processing needed.
|
|
130
|
+
return outputPath;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Create an XLSX file with encrypted base64 content hidden in a veryHidden sheet (legacy v3)
|
|
15
135
|
* @param {object} options - Options for creating the XLSX
|
|
16
136
|
* @param {string} options.base64Content - Encrypted base64 content to store
|
|
17
137
|
* @param {string} options.encryptionMeta - Packed encryption metadata (iv:salt:authTag)
|
|
@@ -122,7 +242,8 @@ async function createXlsxWithBase64(options) {
|
|
|
122
242
|
}
|
|
123
243
|
|
|
124
244
|
/**
|
|
125
|
-
* Read an XLSX file and extract encrypted base64 content and metadata
|
|
245
|
+
* Read an XLSX file and extract encrypted base64 content and metadata.
|
|
246
|
+
* Always uses direct XML extraction for reliability and memory efficiency.
|
|
126
247
|
* @param {string} xlsxPath - Path to XLSX file
|
|
127
248
|
* @returns {Promise<object>} Object containing base64Content, encryptionMeta, and metadata
|
|
128
249
|
*/
|
|
@@ -131,64 +252,7 @@ async function readXlsxBase64(xlsxPath) {
|
|
|
131
252
|
throw new Error(`XLSX file not found: ${xlsxPath}`);
|
|
132
253
|
}
|
|
133
254
|
|
|
134
|
-
|
|
135
|
-
// (ExcelJS fails when namespace prefixes are changed, e.g., ns0:)
|
|
136
|
-
let hiddenSheet = null;
|
|
137
|
-
try {
|
|
138
|
-
const workbook = new ExcelJS.Workbook();
|
|
139
|
-
await workbook.xlsx.readFile(xlsxPath);
|
|
140
|
-
|
|
141
|
-
// Find the hidden sheet
|
|
142
|
-
workbook.eachSheet((sheet) => {
|
|
143
|
-
if (sheet.name === HIDDEN_SHEET_NAME || sheet.state === 'veryHidden') {
|
|
144
|
-
hiddenSheet = sheet;
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
} catch (excelError) {
|
|
148
|
-
// ExcelJS failed (likely namespace issue), use direct XML extraction
|
|
149
|
-
return await extractFromXml(xlsxPath);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// If not found via ExcelJS (veryHidden might be invisible), try direct XML extraction
|
|
153
|
-
if (!hiddenSheet) {
|
|
154
|
-
return await extractFromXml(xlsxPath);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Extract encryption metadata from A1 (may be empty if unencrypted)
|
|
158
|
-
const encryptionMeta = hiddenSheet.getCell('A1').value || '';
|
|
159
|
-
|
|
160
|
-
// Extract serialized metadata from B1
|
|
161
|
-
const metadata = hiddenSheet.getCell('B1').value;
|
|
162
|
-
if (!metadata) {
|
|
163
|
-
throw new Error('No metadata found in XLSX file.');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Get chunk count from C1
|
|
167
|
-
const chunkCountStr = hiddenSheet.getCell('C1').value;
|
|
168
|
-
const chunkCount = parseInt(chunkCountStr, 10);
|
|
169
|
-
|
|
170
|
-
if (isNaN(chunkCount) || chunkCount <= 0) {
|
|
171
|
-
throw new Error('Invalid chunk count in XLSX file.');
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Reconstruct base64 content from chunks
|
|
175
|
-
const chunks = [];
|
|
176
|
-
for (let i = 0; i < chunkCount; i++) {
|
|
177
|
-
const row = Math.floor(i / 26) + 2;
|
|
178
|
-
const col = (i % 26) + 1;
|
|
179
|
-
const chunk = hiddenSheet.getCell(row, col).value;
|
|
180
|
-
if (chunk) {
|
|
181
|
-
chunks.push(chunk);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const base64Content = chunks.join('');
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
base64Content,
|
|
189
|
-
encryptionMeta,
|
|
190
|
-
metadata,
|
|
191
|
-
};
|
|
255
|
+
return await extractFromXml(xlsxPath);
|
|
192
256
|
}
|
|
193
257
|
|
|
194
258
|
/**
|
|
@@ -211,7 +275,7 @@ async function extractFromXml(xlsxPath) {
|
|
|
211
275
|
const sheetParsed = parseXmlFromZip(xlsxPath, 'xl/worksheets/sheet2.xml');
|
|
212
276
|
|
|
213
277
|
if (!sheetParsed) {
|
|
214
|
-
throw new Error('Hidden sheet not found in XLSX file. This may not be a
|
|
278
|
+
throw new Error('Hidden sheet not found in XLSX file. This may not be a stegdoc-encoded file.');
|
|
215
279
|
}
|
|
216
280
|
|
|
217
281
|
// Build cell map from worksheet > sheetData > row > c
|
|
@@ -227,7 +291,7 @@ async function extractFromXml(xlsxPath) {
|
|
|
227
291
|
|
|
228
292
|
for (const cell of cells) {
|
|
229
293
|
const cellRef = cell['@_r']; // e.g., "A1", "B1"
|
|
230
|
-
const cellType = cell['@_t']; // "s" for shared string
|
|
294
|
+
const cellType = cell['@_t']; // "s" for shared string, "inlineStr" for inline
|
|
231
295
|
const cellValue = cell.v;
|
|
232
296
|
|
|
233
297
|
if (cellRef === undefined) continue;
|
|
@@ -238,6 +302,12 @@ async function extractFromXml(xlsxPath) {
|
|
|
238
302
|
if (ssIndex < sharedStrings.length) {
|
|
239
303
|
cellValues.set(cellRef, sharedStrings[ssIndex]);
|
|
240
304
|
}
|
|
305
|
+
} else if (cellType === 'inlineStr' && cell.is) {
|
|
306
|
+
// Inline string (from WorkbookWriter with useSharedStrings: false)
|
|
307
|
+
const text = extractTextContent(cell.is.t);
|
|
308
|
+
if (text !== undefined) {
|
|
309
|
+
cellValues.set(cellRef, text);
|
|
310
|
+
}
|
|
241
311
|
} else if (cellValue !== undefined) {
|
|
242
312
|
cellValues.set(cellRef, String(cellValue));
|
|
243
313
|
}
|
|
@@ -339,21 +409,8 @@ function columnToLetter(col) {
|
|
|
339
409
|
return letter;
|
|
340
410
|
}
|
|
341
411
|
|
|
342
|
-
/**
|
|
343
|
-
* Decode XML entities
|
|
344
|
-
* @param {string} str - String with XML entities
|
|
345
|
-
* @returns {string} Decoded string
|
|
346
|
-
*/
|
|
347
|
-
function decodeXmlEntities(str) {
|
|
348
|
-
return str
|
|
349
|
-
.replace(/"/g, '"')
|
|
350
|
-
.replace(/'/g, "'")
|
|
351
|
-
.replace(/</g, '<')
|
|
352
|
-
.replace(/>/g, '>')
|
|
353
|
-
.replace(/&/g, '&');
|
|
354
|
-
}
|
|
355
|
-
|
|
356
412
|
module.exports = {
|
|
413
|
+
createXlsxPartStreaming,
|
|
357
414
|
createXlsxWithBase64,
|
|
358
415
|
readXlsxBase64,
|
|
359
416
|
};
|
package/bootstrap.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// bootstrap.js - Decode whitener DOCX
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const JSZip = require('jszip');
|
|
4
|
-
|
|
5
|
-
const docxPath = process.argv[2];
|
|
6
|
-
if (!docxPath) {
|
|
7
|
-
console.log('Usage: node bootstrap.js <file.docx>');
|
|
8
|
-
process.exit(1);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
JSZip.loadAsync(fs.readFileSync(docxPath)).then(zip => {
|
|
12
|
-
return zip.file('word/document.xml').async('string');
|
|
13
|
-
}).then(xml => {
|
|
14
|
-
const matches = xml.match(/<w:t[^>]*>([^<]*)<\/w:t>/g) || [];
|
|
15
|
-
let text = '';
|
|
16
|
-
matches.forEach(m => {
|
|
17
|
-
let t = m.replace(/<w:t[^>]*>/, '').replace(/<\/w:t>/, '')
|
|
18
|
-
.replace(/"/g, '"').replace(/'/g, "'")
|
|
19
|
-
.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
|
20
|
-
text += t;
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const metaIdx = text.indexOf('WHITENER_METADATA:');
|
|
24
|
-
const sepIdx = text.indexOf('---', metaIdx);
|
|
25
|
-
const meta = JSON.parse(text.substring(metaIdx + 18, sepIdx).trim());
|
|
26
|
-
const base64 = text.substring(sepIdx + 3).trim();
|
|
27
|
-
|
|
28
|
-
fs.writeFileSync(meta.originalFilename, Buffer.from(base64, 'base64'));
|
|
29
|
-
console.log(`Decoded: ${meta.originalFilename}`);
|
|
30
|
-
}).catch(err => {
|
|
31
|
-
console.error('Error:', err.message);
|
|
32
|
-
process.exit(1);
|
|
33
|
-
});
|