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.
@@ -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 with encrypted base64 content hidden in a veryHidden sheet
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
- // Try ExcelJS first, fall back to direct XML extraction if it fails
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 whitener-encoded file.');
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(/&quot;/g, '"')
350
- .replace(/&apos;/g, "'")
351
- .replace(/&lt;/g, '<')
352
- .replace(/&gt;/g, '>')
353
- .replace(/&amp;/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(/&quot;/g, '"').replace(/&apos;/g, "'")
19
- .replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/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
- });