stegdoc 6.0.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.
@@ -3,14 +3,13 @@ const fs = require('fs');
3
3
  const { pipeline } = require('stream/promises');
4
4
  const chalk = require('chalk');
5
5
  const ora = require('ora');
6
- const AdmZip = require('adm-zip');
7
- const { createDocxWithBase64, createDocxV5 } = require('../lib/docx-handler');
6
+ const { createDocxV5 } = require('../lib/docx-handler');
8
7
  const { createXlsxPartV5 } = require('../lib/xlsx-handler');
9
8
  const { createMetadata, serializeMetadata } = require('../lib/metadata');
10
9
  const crypto = require('crypto');
11
10
  const { generateHash, parseSizeToBytes, formatBytes, generateFilename } = require('../lib/utils');
12
11
  const { packEncryptionMeta, generateSalt, createEncryptStream } = require('../lib/crypto');
13
- const { isCompressedMime, createCompressStream, createBrotliCompressStream } = require('../lib/compression');
12
+ const { createBrotliCompressStream } = require('../lib/compression');
14
13
  const { resetTimeWindow } = require('../lib/decoy-generator');
15
14
  const { resetTimeState, BYTES_PER_DATA_LINE, calculateDataLineCount } = require('../lib/log-generator');
16
15
  const { shouldRunInteractive, promptEncodeOptions } = require('../lib/interactive');
@@ -18,12 +17,52 @@ const { BinaryChunkCollector, ProgressTransform } = require('../lib/streams');
18
17
  const { loadNative } = require('../lib/native');
19
18
 
20
19
  /**
21
- * Zip a folder into a buffer
20
+ * Load the native engine, failing loudly when it is unavailable. Bundling has
21
+ * no JavaScript implementation any more: the archive is built by the core.
22
+ *
23
+ * @returns {object} The native binding.
24
+ */
25
+ function requireNative() {
26
+ const native = loadNative();
27
+ if (!native || typeof native.zip !== 'function') {
28
+ throw new Error(
29
+ 'Bundling inputs requires the native engine, which is not installed. ' +
30
+ 'Reinstall stegdoc, or build it with `pnpm build:native`.'
31
+ );
32
+ }
33
+ return native;
34
+ }
35
+
36
+ /**
37
+ * Recursively collect a directory's files as archive entries under `prefix`.
38
+ *
39
+ * @param {string} dirPath Directory to walk.
40
+ * @param {string} prefix Forward-slash entry path prefix.
41
+ * @param {Array<{name: string, bytes: Buffer}>} entries Accumulator.
42
+ */
43
+ function collectDirectory(dirPath, prefix, entries) {
44
+ for (const dirent of fs.readdirSync(dirPath, { withFileTypes: true })) {
45
+ const full = path.join(dirPath, dirent.name);
46
+ const name = `${prefix}/${dirent.name}`;
47
+ if (dirent.isDirectory()) {
48
+ collectDirectory(full, name, entries);
49
+ } else {
50
+ entries.push({ name, bytes: fs.readFileSync(full) });
51
+ }
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Zip a folder into a buffer, keeping the folder's basename as a prefix.
57
+ *
58
+ * @param {string} folderPath Folder to archive.
59
+ * @returns {Buffer} The archive bytes.
22
60
  */
23
61
  function zipFolder(folderPath) {
24
- const zip = new AdmZip();
25
- zip.addLocalFolder(folderPath);
26
- return zip.toBuffer();
62
+ const native = requireNative();
63
+ const entries = [];
64
+ collectDirectory(folderPath, path.basename(folderPath), entries);
65
+ return native.zip(entries);
27
66
  }
28
67
 
29
68
  /**
@@ -59,18 +98,19 @@ function uniqueEntryName(name, used) {
59
98
  * @returns {Buffer} The archive bytes.
60
99
  */
61
100
  function zipInputs(inputs) {
62
- const zip = new AdmZip();
101
+ const native = requireNative();
102
+ const entries = [];
63
103
  const used = new Set();
64
104
  for (const input of inputs) {
65
105
  const base = path.basename(input.replace(/[\\/]+$/, ''));
66
106
  const entry = uniqueEntryName(base, used);
67
107
  if (fs.statSync(input).isDirectory()) {
68
- zip.addLocalFolder(input, entry);
108
+ collectDirectory(input, entry, entries);
69
109
  } else {
70
- zip.addLocalFile(input, '', entry);
110
+ entries.push({ name: entry, bytes: fs.readFileSync(input) });
71
111
  }
72
112
  }
73
- return zip.toBuffer();
113
+ return native.zip(entries);
74
114
  }
75
115
 
76
116
  /**
@@ -86,21 +126,23 @@ function bundleFilename(name) {
86
126
  }
87
127
 
88
128
  /**
89
- * Detect file type from the first 4KB
129
+ * Extensions whose contents are already compressed, so Brotli only wastes CPU.
90
130
  */
91
- async function detectFileType(filePath) {
92
- try {
93
- const fileType = await import('file-type');
94
- const fromBuffer = fileType.fileTypeFromBuffer || fileType.default?.fromBuffer;
95
- if (!fromBuffer) return null;
96
- const fd = await fs.promises.open(filePath, 'r');
97
- const buf = Buffer.alloc(4100);
98
- await fd.read(buf, 0, 4100, 0);
99
- await fd.close();
100
- return await fromBuffer(buf);
101
- } catch {
102
- return null;
103
- }
131
+ const COMPRESSED_EXTENSIONS = new Set([
132
+ 'zip', 'gz', 'tgz', 'bz2', 'xz', '7z', 'rar',
133
+ 'jpg', 'jpeg', 'png', 'gif', 'webp',
134
+ 'mp3', 'mp4', 'm4a', 'mov', 'avi', 'mkv', 'webm', 'ogg', 'opus', 'flac',
135
+ 'pdf', 'apk', 'jar', 'docx', 'xlsx', 'pptx', 'wasm', 'br',
136
+ ]);
137
+
138
+ /**
139
+ * Check whether an extension is already compressed.
140
+ *
141
+ * @param {string} ext Extension, with or without a leading dot.
142
+ * @returns {boolean} True when the format needs no further compression.
143
+ */
144
+ function isCompressedExtension(ext) {
145
+ return COMPRESSED_EXTENSIONS.has(String(ext || '').toLowerCase().replace(/^\./, ''));
104
146
  }
105
147
 
106
148
  /**
@@ -143,13 +185,9 @@ async function encodeCommand(input, options) {
143
185
  }
144
186
 
145
187
  const quiet = options.quiet || false;
146
- const legacy = options.legacy || false;
147
188
  if (options.v5 && options.v6) {
148
189
  throw new Error('--v5 and --v6 cannot be combined.');
149
190
  }
150
- if (legacy && (options.v5 || options.v6)) {
151
- throw new Error('--legacy cannot be combined with --v5 or --v6.');
152
- }
153
191
  const version = options.v5 ? 'v5' : 'v6';
154
192
  const spinner = quiet ? { start: () => {}, succeed: () => {}, fail: () => {}, info: () => {}, text: '' } : ora('Starting encoding process...').start();
155
193
  const createdFiles = [];
@@ -220,33 +258,22 @@ async function encodeCommand(input, options) {
220
258
 
221
259
  spinner.text = 'Checking file type...';
222
260
  let useCompression = true;
223
- const fileType = await detectFileType(streamSource);
261
+ const extensionKey = extension.replace(/^\./, '').toLowerCase();
224
262
 
225
- if (fileType && isCompressedMime(fileType.mime)) {
263
+ if (isCompressedExtension(extensionKey)) {
226
264
  useCompression = false;
227
- spinner.info && spinner.info(`Skipping compression (${fileType.ext} is already compressed)`);
265
+ spinner.info && spinner.info(`Skipping compression (${extensionKey} is already compressed)`);
228
266
  }
229
267
 
230
- // DOCX v5 size limit (not applicable in legacy mode or with --no-limit)
268
+ // DOCX v5 size limit (not applicable with --no-limit)
231
269
  const noLimit = options.noLimit || options.limit === false;
232
- if (format === 'docx' && !legacy && !noLimit && fileSize > 1 * 1024 * 1024) {
270
+ if (format === 'docx' && !noLimit && fileSize > 1 * 1024 * 1024) {
233
271
  throw new Error(
234
272
  `DOCX format is limited to files under 1 MB (yours is ${formatBytes(fileSize)}). ` +
235
273
  `Use XLSX format (-f xlsx) for larger files, or --no-limit to bypass.`
236
274
  );
237
275
  }
238
276
 
239
- // Route to legacy or v5 pipeline
240
- if (legacy) {
241
- if (format === 'docx') {
242
- await encodeLegacyDocx(streamSource, filename, extension, fileSize, options, useCompression, useEncryption, spinner, quiet, createdFiles);
243
- } else {
244
- throw new Error('Legacy XLSX format (--legacy) is no longer supported. Use v5 format (default) or legacy DOCX (-f docx --legacy).');
245
- }
246
- if (tempZipPath) cleanupTemp(tempZipPath);
247
- return;
248
- }
249
-
250
277
  // === Log-Embed Pipeline ===
251
278
  const hash = generateHash();
252
279
  const outputDir = options.outputDir || process.cwd();
@@ -274,7 +301,7 @@ async function encodeCommand(input, options) {
274
301
  // Pre-compute content hash
275
302
  spinner.text = 'Computing file hash...';
276
303
 
277
- // Prefer the native engine. Legacy mode returned above.
304
+ // Prefer the native engine; the JavaScript encoder only covers v5.
278
305
  const native = loadNative();
279
306
  if (version === 'v6' && !native) {
280
307
  throw new Error(
@@ -393,7 +420,6 @@ async function encodeCommand(input, options) {
393
420
  encrypted: useEncryption,
394
421
  compressed: useCompression,
395
422
  contentHash,
396
- stegoMethod: 'log-embed',
397
423
  compressionAlgo: 'brotli',
398
424
  payloadSize: payloadBuffer.length,
399
425
  dataLineCount,
@@ -497,96 +523,6 @@ async function encodeCommand(input, options) {
497
523
  }
498
524
  }
499
525
 
500
- // ─── Legacy v4 DOCX Pipeline ────────────────────────────────────────────────
501
-
502
- async function encodeLegacyDocx(inputPath, filename, extension, fileSize, options, useCompression, useEncryption, spinner, quiet, createdFiles) {
503
- const { compress } = require('../lib/compression');
504
- const { encrypt, packEncryptionMeta: packMeta } = require('../lib/crypto');
505
- const { generateContentHash } = require('../lib/utils');
506
-
507
- const fileBuffer = fs.readFileSync(inputPath);
508
- const contentHash = generateContentHash(fileBuffer);
509
-
510
- let processedBuffer = fileBuffer;
511
- if (useCompression) {
512
- spinner.text = 'Compressing...';
513
- const compressedBuffer = await compress(fileBuffer);
514
- if (compressedBuffer.length < fileBuffer.length) {
515
- processedBuffer = compressedBuffer;
516
- spinner.succeed && spinner.succeed(`Compressed: ${formatBytes(fileBuffer.length)} → ${formatBytes(compressedBuffer.length)}`);
517
- } else {
518
- useCompression = false;
519
- spinner.info && spinner.info('Compression skipped (no size benefit)');
520
- }
521
- }
522
-
523
- const base64 = processedBuffer.toString('base64');
524
-
525
- let contentToStore;
526
- let encryptionMeta = null;
527
-
528
- if (useEncryption) {
529
- spinner.text = 'Encrypting content...';
530
- const { ciphertext, iv, salt, authTag } = encrypt(base64, options.password);
531
- encryptionMeta = packMeta({ iv, salt, authTag });
532
- contentToStore = ciphertext;
533
- spinner.succeed && spinner.succeed('Content encrypted with AES-256-GCM');
534
- } else {
535
- contentToStore = base64;
536
- }
537
-
538
- const hash = generateHash();
539
- const outputDir = options.outputDir || process.cwd();
540
- const format = 'docx';
541
-
542
- const metadata = createMetadata({
543
- originalFilename: filename,
544
- originalExtension: extension,
545
- hash,
546
- partNumber: null,
547
- totalParts: null,
548
- originalSize: fileSize,
549
- format,
550
- encrypted: useEncryption,
551
- compressed: useCompression,
552
- contentHash,
553
- });
554
-
555
- const outputFilename = generateFilename(hash, null, null, format);
556
- const outputPath = path.join(outputDir, outputFilename);
557
-
558
- if (fs.existsSync(outputPath) && !options.force) {
559
- throw new Error(`File already exists: ${outputPath}. Use --force to overwrite.`);
560
- }
561
-
562
- const docxContent = useEncryption
563
- ? `${encryptionMeta}|||${contentToStore}`
564
- : contentToStore;
565
-
566
- await createDocxWithBase64({
567
- base64Content: docxContent,
568
- metadata,
569
- outputPath,
570
- });
571
-
572
- createdFiles.push(outputPath);
573
- spinner.succeed && spinner.succeed('Encoding complete!');
574
-
575
- if (!quiet) {
576
- console.log();
577
- console.log(chalk.green.bold('✓ File encoded successfully!'));
578
- console.log(chalk.cyan(` Format: DOCX (v4 legacy)`));
579
- console.log(chalk.cyan(` Hash: ${hash}`));
580
- console.log(chalk.cyan(` Output: ${outputFilename}`));
581
- console.log(chalk.cyan(` Encrypted: ${useEncryption ? 'Yes' : 'No'}`));
582
- console.log(chalk.cyan(` Compressed: ${useCompression ? 'Yes' : 'No'}`));
583
- console.log(chalk.cyan(` Location: ${outputDir}`));
584
- if (useEncryption) {
585
- console.log(chalk.yellow(` Remember your password - it cannot be recovered!`));
586
- }
587
- }
588
- }
589
-
590
526
  function cleanupTemp(tempPath) {
591
527
  try {
592
528
  if (fs.existsSync(tempPath)) {
@@ -1,13 +1,15 @@
1
- const path = require('path');
2
1
  const chalk = require('chalk');
3
2
  const ora = require('ora');
4
- const { readDocxBase64 } = require('../lib/docx-handler');
5
- const { readXlsxBase64 } = require('../lib/xlsx-handler');
6
- const { validateMetadata, isMultiPart, isLogEmbedFormat } = require('../lib/metadata');
7
- const { detectFormat, formatBytes } = require('../lib/utils');
8
- const { extractContent, findMultiPartFiles } = require('../lib/file-utils');
3
+ const { formatBytes } = require('../lib/utils');
9
4
  const { loadNative } = require('../lib/native');
10
5
 
6
+ const NATIVE_REQUIRED =
7
+ 'Reading file metadata requires the native engine, which is not installed. ' +
8
+ 'Reinstall stegdoc, or build it with `pnpm build:native`.';
9
+
10
+ const LEGACY_UNSUPPORTED =
11
+ 'The legacy v3/v4 format is no longer supported. Re-encode the file with a current version of stegdoc.';
12
+
11
13
  /**
12
14
  * Print metadata read by the native engine, which handles v5 and v6.
13
15
  */
@@ -54,109 +56,29 @@ async function infoCommand(inputFile, options) {
54
56
  const spinner = ora('Reading file metadata...').start();
55
57
 
56
58
  try {
57
- const format = detectFormat(inputFile);
58
- if (!format) {
59
- throw new Error('Unknown file format. Supported formats: .xlsx, .docx');
60
- }
61
-
62
- // The native engine reads v5 and v6; the JS reader only knows v5.
63
59
  const native = loadNative();
64
- if (native && native.probe(inputFile) === 'v5') {
65
- const info = native.info(inputFile);
66
- spinner.succeed('File metadata read successfully');
67
- printNativeInfo(info);
68
- return;
69
- }
70
-
71
- let readResult;
72
- if (format === 'xlsx') {
73
- readResult = await readXlsxBase64(inputFile);
74
- } else {
75
- readResult = await readDocxBase64(inputFile);
76
- }
77
-
78
- const extracted = extractContent(readResult, format);
79
- const metadata = extracted.metadata;
80
- const encryptionMeta = extracted.encryptionMeta;
81
-
82
- validateMetadata(metadata);
83
-
84
- const isEncrypted = metadata.encrypted || (encryptionMeta && encryptionMeta.length > 0);
85
- const isCompressed = metadata.compressed || false;
86
- const isV5 = isLogEmbedFormat(metadata);
87
-
88
- spinner.succeed('File metadata read successfully');
89
- console.log();
90
-
91
- console.log(chalk.bold.white('File Information:'));
92
- console.log(chalk.cyan(` Format: ${format.toUpperCase()}`));
93
- console.log(chalk.cyan(` Tool version: ${metadata.version || '1.x'}`));
94
- if (isV5) {
95
- console.log(chalk.cyan(` Stego method: Log-embed (v5)`));
96
- console.log(chalk.cyan(` Compression: ${metadata.compressionAlgo || 'brotli'}`));
60
+ if (!native) {
61
+ throw new Error(NATIVE_REQUIRED);
97
62
  }
98
- console.log();
99
-
100
- console.log(chalk.bold.white('Original File:'));
101
- console.log(chalk.cyan(` Filename: ${metadata.originalFilename}`));
102
- console.log(chalk.cyan(` Extension: ${metadata.originalExtension}`));
103
- console.log(chalk.cyan(` Size: ${formatBytes(metadata.originalSize)}`));
104
- console.log();
105
-
106
- console.log(chalk.bold.white('Encoding Options:'));
107
- console.log(chalk.cyan(` Encrypted: ${isEncrypted ? chalk.yellow('Yes') : 'No'}`));
108
- console.log(chalk.cyan(` Compressed: ${isCompressed ? chalk.green('Yes') : 'No'}`));
109
- console.log(chalk.cyan(` Encoded on: ${metadata.encodingDate || 'Unknown'}`));
110
63
 
111
- if (metadata.contentHash) {
112
- console.log(chalk.cyan(` Content hash: ${metadata.contentHash.slice(0, 16)}...`));
64
+ let probe;
65
+ try {
66
+ probe = native.probe(inputFile);
67
+ } catch {
68
+ probe = 'unknown';
113
69
  }
114
-
115
- if (isV5 && metadata.dataLineCount) {
116
- console.log(chalk.cyan(` Data lines: ${metadata.dataLineCount}`));
117
- }
118
- console.log();
119
-
120
- const hasMultipleParts = isMultiPart(metadata) || metadata.partNumber !== null;
121
- if (hasMultipleParts) {
122
- console.log(chalk.bold.white('Multi-part File:'));
123
- const inputDir = path.dirname(inputFile);
124
- const allParts = findMultiPartFiles(inputDir, metadata.hash, format);
125
- const totalParts = metadata.totalParts || allParts.length;
126
-
127
- console.log(chalk.cyan(` This is part: ${metadata.partNumber} of ${totalParts}`));
128
- console.log(chalk.cyan(` Hash: ${metadata.hash}`));
129
-
130
- console.log();
131
- console.log(chalk.bold.white('Parts found in directory:'));
132
-
133
- for (let i = 1; i <= totalParts; i++) {
134
- const part = allParts.find(p => p.partNumber === i);
135
- if (part) {
136
- console.log(chalk.green(` ✓ Part ${i}: ${part.filename}`));
137
- } else {
138
- console.log(chalk.red(` ✗ Part ${i}: MISSING`));
139
- }
140
- }
141
-
142
- if (allParts.length >= totalParts) {
143
- console.log();
144
- console.log(chalk.green.bold('All parts found - ready to decode'));
145
- } else {
146
- console.log();
147
- console.log(chalk.yellow.bold(`Missing ${totalParts - allParts.length} part(s)`));
148
- }
149
- } else {
150
- console.log(chalk.bold.white('Single File:'));
151
- console.log(chalk.cyan(` Hash: ${metadata.hash}`));
152
- console.log();
153
- console.log(chalk.green.bold('Ready to decode'));
70
+ if (probe === 'legacy') {
71
+ throw new Error(LEGACY_UNSUPPORTED);
154
72
  }
155
-
156
- if (isEncrypted) {
157
- console.log(chalk.yellow('\nNote: Password required for decoding'));
73
+ if (probe !== 'v5' && probe !== 'archive') {
74
+ throw new Error(
75
+ 'Unknown file format. Supported formats: .xlsx, .docx, or a zip of them'
76
+ );
158
77
  }
159
78
 
79
+ const info = native.info(inputFile);
80
+ spinner.succeed('File metadata read successfully');
81
+ printNativeInfo(info);
160
82
  } catch (error) {
161
83
  spinner.fail('Failed to read file info');
162
84
  console.error(chalk.red(`Error: ${error.message}`));
@@ -1,14 +1,15 @@
1
- const path = require('path');
2
1
  const chalk = require('chalk');
3
2
  const ora = require('ora');
4
- const { readDocxBase64 } = require('../lib/docx-handler');
5
- const { readXlsxBase64 } = require('../lib/xlsx-handler');
6
- const { validateMetadata, isMultiPart, isStreamingFormat, isLogEmbedFormat } = require('../lib/metadata');
7
- const { detectFormat, formatBytes } = require('../lib/utils');
8
- const { decrypt, unpackEncryptionMeta, createDecryptStream } = require('../lib/crypto');
9
- const { extractContent, findMultiPartFiles } = require('../lib/file-utils');
3
+ const { formatBytes } = require('../lib/utils');
10
4
  const { loadNative } = require('../lib/native');
11
5
 
6
+ const NATIVE_REQUIRED =
7
+ 'Verifying a file requires the native engine, which is not installed. ' +
8
+ 'Reinstall stegdoc, or build it with `pnpm build:native`.';
9
+
10
+ const LEGACY_UNSUPPORTED =
11
+ 'The legacy v3/v4 format is no longer supported. Re-encode the file with a current version of stegdoc.';
12
+
12
13
  /**
13
14
  * Verify a v5/v6 file with the native engine, which also reads v6.
14
15
  */
@@ -73,133 +74,29 @@ function verifyNative(native, inputFile, options, spinner) {
73
74
  */
74
75
  async function verifyCommand(inputFile, options) {
75
76
  const spinner = ora('Verifying file...').start();
76
- const issues = [];
77
77
 
78
78
  try {
79
- const format = detectFormat(inputFile);
80
- if (!format) {
81
- throw new Error('Unknown file format. Supported formats: .xlsx, .docx');
82
- }
83
-
84
- // The native engine reads v5 and v6; the JS reader only knows v5.
85
79
  const native = loadNative();
86
- if (native && native.probe(inputFile) === 'v5') {
87
- return verifyNative(native, inputFile, options, spinner);
88
- }
89
-
90
- spinner.text = `Reading ${format.toUpperCase()} file...`;
91
-
92
- let readResult;
93
- if (format === 'xlsx') {
94
- readResult = await readXlsxBase64(inputFile);
95
- } else {
96
- readResult = await readDocxBase64(inputFile);
80
+ if (!native) {
81
+ throw new Error(NATIVE_REQUIRED);
97
82
  }
98
83
 
99
- const extracted = extractContent(readResult, format);
100
- const metadata = extracted.metadata;
101
- const encryptionMeta = extracted.encryptionMeta;
102
-
84
+ let probe;
103
85
  try {
104
- validateMetadata(metadata);
105
- spinner.succeed('Metadata valid');
106
- } catch (e) {
107
- issues.push(`Metadata: ${e.message}`);
108
- spinner.warn('Metadata issues found');
86
+ probe = native.probe(inputFile);
87
+ } catch {
88
+ probe = 'unknown';
109
89
  }
110
-
111
- const isEncrypted = metadata.encrypted || (encryptionMeta && encryptionMeta.length > 0);
112
- const isV5 = isLogEmbedFormat(metadata);
113
- const isV4 = !isV5 && isStreamingFormat(metadata);
114
-
115
- // Check multi-part
116
- const hasMultipleParts = isMultiPart(metadata) || metadata.partNumber !== null;
117
- if (hasMultipleParts) {
118
- spinner.text = 'Checking multi-part files...';
119
- const inputDir = path.dirname(inputFile);
120
- const allParts = findMultiPartFiles(inputDir, metadata.hash, format);
121
- const totalParts = metadata.totalParts || allParts.length;
122
-
123
- if (metadata.totalParts !== null && allParts.length !== metadata.totalParts) {
124
- const missing = [];
125
- const foundParts = new Set(allParts.map(p => p.partNumber));
126
- for (let i = 1; i <= metadata.totalParts; i++) {
127
- if (!foundParts.has(i)) missing.push(i);
128
- }
129
- issues.push(`Missing parts: ${missing.join(', ')}`);
130
- spinner.warn(`Found ${allParts.length}/${metadata.totalParts} parts`);
131
- } else {
132
- spinner.succeed(`All ${totalParts} parts found`);
133
- }
134
- }
135
-
136
- // Check password for encrypted files
137
- if (isEncrypted) {
138
- if (!options.password) {
139
- issues.push('File is encrypted but no password provided');
140
- spinner.warn('Encryption check skipped (no password)');
141
- } else {
142
- spinner.text = 'Verifying decryption...';
143
- try {
144
- if (isV5) {
145
- await verifyV5Encryption(extracted, options.password);
146
- } else if (isV4) {
147
- await verifyV4Encryption(inputFile, format, encryptionMeta, options.password);
148
- } else {
149
- await verifyV3Encryption(inputFile, format, metadata, extracted.encryptedContent, encryptionMeta, options.password);
150
- }
151
- spinner.succeed('Decryption password valid');
152
- } catch (e) {
153
- issues.push('Decryption failed - wrong password or corrupted data');
154
- spinner.fail('Decryption check failed');
155
- }
156
- }
90
+ if (probe === 'legacy') {
91
+ throw new Error(LEGACY_UNSUPPORTED);
157
92
  }
158
-
159
- // Summary
160
- console.log();
161
-
162
- if (issues.length === 0) {
163
- console.log(chalk.green.bold('✓ File verification passed!'));
164
- console.log(chalk.cyan(` Original file: ${metadata.originalFilename}`));
165
- console.log(chalk.cyan(` Size: ${formatBytes(metadata.originalSize)}`));
166
- console.log(chalk.cyan(` Encrypted: ${isEncrypted ? 'Yes' : 'No'}`));
167
- console.log(chalk.cyan(` Compressed: ${metadata.compressed ? 'Yes' : 'No'}`));
168
-
169
- let versionStr = 'v3 (legacy)';
170
- if (isV5) versionStr = 'v5 (log-embed)';
171
- else if (isV4) versionStr = 'v4 (streaming)';
172
- console.log(chalk.cyan(` Format version: ${versionStr}`));
173
-
174
- if (hasMultipleParts) {
175
- const totalParts = metadata.totalParts || 'unknown';
176
- console.log(chalk.cyan(` Parts: ${totalParts}`));
177
- }
178
-
179
- console.log();
180
- console.log(chalk.green('File is ready to decode.'));
181
- } else {
182
- console.log(chalk.yellow.bold('⚠ Verification completed with issues:'));
183
- console.log();
184
- for (const issue of issues) {
185
- console.log(chalk.yellow(` • ${issue}`));
186
- }
187
- console.log();
188
-
189
- const hasBlockingIssue = issues.some(i =>
190
- i.includes('Missing parts') ||
191
- i.includes('Decryption failed') ||
192
- i.includes('Metadata')
93
+ if (probe !== 'v5' && probe !== 'archive') {
94
+ throw new Error(
95
+ 'Unknown file format. Supported formats: .xlsx, .docx, or a zip of them'
193
96
  );
194
-
195
- if (hasBlockingIssue) {
196
- console.log(chalk.red('File cannot be decoded until issues are resolved.'));
197
- process.exit(1);
198
- } else {
199
- console.log(chalk.yellow('File may still be decodable. Run decode to attempt.'));
200
- }
201
97
  }
202
98
 
99
+ return verifyNative(native, inputFile, options, spinner);
203
100
  } catch (error) {
204
101
  spinner.fail('Verification failed');
205
102
  console.error(chalk.red(`Error: ${error.message}`));
@@ -207,67 +104,4 @@ async function verifyCommand(inputFile, options) {
207
104
  }
208
105
  }
209
106
 
210
- /**
211
- * Verify v5 log-embed encryption
212
- */
213
- async function verifyV5Encryption(extracted, password) {
214
- const { payloadBuffer, encryptionMeta } = extracted;
215
- const { iv, salt, authTag } = unpackEncryptionMeta(encryptionMeta);
216
- const decipher = createDecryptStream(password, iv, salt, authTag);
217
- decipher.update(payloadBuffer);
218
- decipher.final();
219
- }
220
-
221
- /**
222
- * Verify v4 per-part encryption
223
- */
224
- async function verifyV4Encryption(inputFile, format, encryptionMeta, password) {
225
- const { iv, salt, authTag } = unpackEncryptionMeta(encryptionMeta);
226
-
227
- let readResult;
228
- if (format === 'xlsx') {
229
- readResult = await readXlsxBase64(inputFile);
230
- } else {
231
- readResult = await readDocxBase64(inputFile);
232
- }
233
-
234
- const { encryptedContent } = extractContent(readResult, format);
235
- const binaryData = Buffer.from(encryptedContent, 'base64');
236
-
237
- const decipher = createDecryptStream(password, iv, salt, authTag);
238
- decipher.update(binaryData);
239
- decipher.final();
240
- }
241
-
242
- /**
243
- * Verify v3 shared encryption
244
- */
245
- async function verifyV3Encryption(inputFile, format, metadata, encryptedContent, encryptionMeta, password) {
246
- const { iv, salt, authTag } = unpackEncryptionMeta(encryptionMeta);
247
-
248
- let fullContent = encryptedContent;
249
-
250
- if (isMultiPart(metadata)) {
251
- const inputDir = path.dirname(inputFile);
252
- const allParts = findMultiPartFiles(inputDir, metadata.hash, format);
253
-
254
- if (allParts.length === metadata.totalParts) {
255
- const contentParts = [];
256
- for (const part of allParts) {
257
- let partResult;
258
- if (format === 'xlsx') {
259
- partResult = await readXlsxBase64(part.path);
260
- } else {
261
- partResult = await readDocxBase64(part.path);
262
- }
263
- const { encryptedContent: partContent } = extractContent(partResult, format);
264
- contentParts.push(partContent);
265
- }
266
- fullContent = contentParts.join('');
267
- }
268
- }
269
-
270
- decrypt(fullContent, password, iv, salt, authTag);
271
- }
272
-
273
107
  module.exports = verifyCommand;