stegdoc 4.0.0 → 5.0.1

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.
@@ -1,204 +1,207 @@
1
- const path = require('path');
2
- const chalk = require('chalk');
3
- const ora = require('ora');
4
- const { readDocxBase64 } = require('../lib/docx-handler');
5
- const { readXlsxBase64 } = require('../lib/xlsx-handler');
6
- const { validateMetadata, isMultiPart, isStreamingFormat } = 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');
10
-
11
- /**
12
- * Verify that a file can be decoded without actually writing output
13
- * @param {string} inputFile - Path to input file
14
- * @param {object} options - Command options
15
- * @param {string} options.password - Decryption password (if encrypted)
16
- */
17
- async function verifyCommand(inputFile, options) {
18
- const spinner = ora('Verifying file...').start();
19
- const issues = [];
20
-
21
- try {
22
- // Detect format from extension
23
- const format = detectFormat(inputFile);
24
- if (!format) {
25
- throw new Error('Unknown file format. Supported formats: .xlsx, .docx');
26
- }
27
-
28
- spinner.text = `Reading ${format.toUpperCase()} file...`;
29
-
30
- // Read file
31
- let readResult;
32
- if (format === 'xlsx') {
33
- readResult = await readXlsxBase64(inputFile);
34
- } else {
35
- readResult = await readDocxBase64(inputFile);
36
- }
37
-
38
- const { encryptedContent, encryptionMeta, metadata } = extractContent(readResult, format);
39
-
40
- // Validate metadata
41
- try {
42
- validateMetadata(metadata);
43
- spinner.succeed('Metadata valid');
44
- } catch (e) {
45
- issues.push(`Metadata: ${e.message}`);
46
- spinner.warn('Metadata issues found');
47
- }
48
-
49
- const isEncrypted = metadata.encrypted || (encryptionMeta && encryptionMeta.length > 0);
50
- const isV4 = isStreamingFormat(metadata);
51
-
52
- // Check multi-part
53
- if (isMultiPart(metadata)) {
54
- spinner.text = 'Checking multi-part files...';
55
-
56
- const inputDir = path.dirname(inputFile);
57
- const allParts = findMultiPartFiles(inputDir, metadata.hash, format);
58
-
59
- if (allParts.length !== metadata.totalParts) {
60
- const missing = [];
61
- const foundParts = new Set(allParts.map(p => p.partNumber));
62
-
63
- for (let i = 1; i <= metadata.totalParts; i++) {
64
- if (!foundParts.has(i)) {
65
- missing.push(i);
66
- }
67
- }
68
-
69
- issues.push(`Missing parts: ${missing.join(', ')}`);
70
- spinner.warn(`Found ${allParts.length}/${metadata.totalParts} parts`);
71
- } else {
72
- spinner.succeed(`All ${metadata.totalParts} parts found`);
73
- }
74
- }
75
-
76
- // Check password for encrypted files
77
- if (isEncrypted) {
78
- if (!options.password) {
79
- issues.push('File is encrypted but no password provided');
80
- spinner.warn('Encryption check skipped (no password)');
81
- } else {
82
- spinner.text = 'Verifying decryption...';
83
-
84
- try {
85
- if (isV4) {
86
- // v4: per-part encryption — verify each part independently
87
- await verifyV4Encryption(inputFile, format, metadata, encryptionMeta, options.password, spinner);
88
- } else {
89
- // v3: shared encryption — merge all parts then decrypt
90
- await verifyV3Encryption(inputFile, format, metadata, encryptedContent, encryptionMeta, options.password, spinner);
91
- }
92
- spinner.succeed('Decryption password valid');
93
- } catch (e) {
94
- issues.push('Decryption failed - wrong password or corrupted data');
95
- spinner.fail('Decryption check failed');
96
- }
97
- }
98
- }
99
-
100
- // Summary
101
- console.log();
102
-
103
- if (issues.length === 0) {
104
- console.log(chalk.green.bold('✓ File verification passed!'));
105
- console.log(chalk.cyan(` Original file: ${metadata.originalFilename}`));
106
- console.log(chalk.cyan(` Size: ${formatBytes(metadata.originalSize)}`));
107
- console.log(chalk.cyan(` Encrypted: ${isEncrypted ? 'Yes' : 'No'}`));
108
- console.log(chalk.cyan(` Compressed: ${metadata.compressed ? 'Yes' : 'No'}`));
109
- console.log(chalk.cyan(` Format version: ${isV4 ? 'v4 (streaming)' : 'v3 (legacy)'}`));
110
-
111
- if (isMultiPart(metadata)) {
112
- console.log(chalk.cyan(` Parts: ${metadata.totalParts}`));
113
- }
114
-
115
- console.log();
116
- console.log(chalk.green('File is ready to decode.'));
117
- } else {
118
- console.log(chalk.yellow.bold('⚠ Verification completed with issues:'));
119
- console.log();
120
-
121
- for (const issue of issues) {
122
- console.log(chalk.yellow(` • ${issue}`));
123
- }
124
-
125
- console.log();
126
-
127
- // Determine if still decodable
128
- const hasBlockingIssue = issues.some(i =>
129
- i.includes('Missing parts') ||
130
- i.includes('Decryption failed') ||
131
- i.includes('Metadata')
132
- );
133
-
134
- if (hasBlockingIssue) {
135
- console.log(chalk.red('File cannot be decoded until issues are resolved.'));
136
- process.exit(1);
137
- } else {
138
- console.log(chalk.yellow('File may still be decodable. Run decode to attempt.'));
139
- }
140
- }
141
-
142
- } catch (error) {
143
- spinner.fail('Verification failed');
144
- console.error(chalk.red(`Error: ${error.message}`));
145
- process.exit(1);
146
- }
147
- }
148
-
149
- /**
150
- * Verify v4 per-part encryption by decrypting the first part
151
- */
152
- async function verifyV4Encryption(inputFile, format, metadata, encryptionMeta, password, spinner) {
153
- // For v4, each part has its own encryption metadata
154
- // Verify by decrypting the first part's content
155
- const { iv, salt, authTag } = unpackEncryptionMeta(encryptionMeta);
156
-
157
- // Read the first part's content
158
- let readResult;
159
- if (format === 'xlsx') {
160
- readResult = await readXlsxBase64(inputFile);
161
- } else {
162
- readResult = await readDocxBase64(inputFile);
163
- }
164
-
165
- const { encryptedContent } = extractContent(readResult, format);
166
- const binaryData = Buffer.from(encryptedContent, 'base64');
167
-
168
- const decipher = createDecryptStream(password, iv, salt, authTag);
169
- decipher.update(binaryData);
170
- decipher.final(); // Throws on wrong password
171
- }
172
-
173
- /**
174
- * Verify v3 shared encryption by merging all parts and decrypting
175
- */
176
- async function verifyV3Encryption(inputFile, format, metadata, encryptedContent, encryptionMeta, password, spinner) {
177
- const { iv, salt, authTag } = unpackEncryptionMeta(encryptionMeta);
178
-
179
- let fullContent = encryptedContent;
180
-
181
- if (isMultiPart(metadata)) {
182
- const inputDir = path.dirname(inputFile);
183
- const allParts = findMultiPartFiles(inputDir, metadata.hash, format);
184
-
185
- if (allParts.length === metadata.totalParts) {
186
- const contentParts = [];
187
- for (const part of allParts) {
188
- let partResult;
189
- if (format === 'xlsx') {
190
- partResult = await readXlsxBase64(part.path);
191
- } else {
192
- partResult = await readDocxBase64(part.path);
193
- }
194
- const { encryptedContent: partContent } = extractContent(partResult, format);
195
- contentParts.push(partContent);
196
- }
197
- fullContent = contentParts.join('');
198
- }
199
- }
200
-
201
- decrypt(fullContent, password, iv, salt, authTag);
202
- }
203
-
204
- module.exports = verifyCommand;
1
+ const path = require('path');
2
+ const chalk = require('chalk');
3
+ 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');
10
+
11
+ /**
12
+ * Verify that a file can be decoded without actually writing output
13
+ */
14
+ async function verifyCommand(inputFile, options) {
15
+ const spinner = ora('Verifying file...').start();
16
+ const issues = [];
17
+
18
+ try {
19
+ const format = detectFormat(inputFile);
20
+ if (!format) {
21
+ throw new Error('Unknown file format. Supported formats: .xlsx, .docx');
22
+ }
23
+
24
+ spinner.text = `Reading ${format.toUpperCase()} file...`;
25
+
26
+ let readResult;
27
+ if (format === 'xlsx') {
28
+ readResult = await readXlsxBase64(inputFile);
29
+ } else {
30
+ readResult = await readDocxBase64(inputFile);
31
+ }
32
+
33
+ const extracted = extractContent(readResult, format);
34
+ const metadata = extracted.metadata;
35
+ const encryptionMeta = extracted.encryptionMeta;
36
+
37
+ try {
38
+ validateMetadata(metadata);
39
+ spinner.succeed('Metadata valid');
40
+ } catch (e) {
41
+ issues.push(`Metadata: ${e.message}`);
42
+ spinner.warn('Metadata issues found');
43
+ }
44
+
45
+ const isEncrypted = metadata.encrypted || (encryptionMeta && encryptionMeta.length > 0);
46
+ const isV5 = isLogEmbedFormat(metadata);
47
+ const isV4 = !isV5 && isStreamingFormat(metadata);
48
+
49
+ // Check multi-part
50
+ const hasMultipleParts = isMultiPart(metadata) || metadata.partNumber !== null;
51
+ if (hasMultipleParts) {
52
+ spinner.text = 'Checking multi-part files...';
53
+ const inputDir = path.dirname(inputFile);
54
+ const allParts = findMultiPartFiles(inputDir, metadata.hash, format);
55
+ const totalParts = metadata.totalParts || allParts.length;
56
+
57
+ if (metadata.totalParts !== null && allParts.length !== metadata.totalParts) {
58
+ const missing = [];
59
+ const foundParts = new Set(allParts.map(p => p.partNumber));
60
+ for (let i = 1; i <= metadata.totalParts; i++) {
61
+ if (!foundParts.has(i)) missing.push(i);
62
+ }
63
+ issues.push(`Missing parts: ${missing.join(', ')}`);
64
+ spinner.warn(`Found ${allParts.length}/${metadata.totalParts} parts`);
65
+ } else {
66
+ spinner.succeed(`All ${totalParts} parts found`);
67
+ }
68
+ }
69
+
70
+ // Check password for encrypted files
71
+ if (isEncrypted) {
72
+ if (!options.password) {
73
+ issues.push('File is encrypted but no password provided');
74
+ spinner.warn('Encryption check skipped (no password)');
75
+ } else {
76
+ spinner.text = 'Verifying decryption...';
77
+ try {
78
+ if (isV5) {
79
+ await verifyV5Encryption(extracted, options.password);
80
+ } else if (isV4) {
81
+ await verifyV4Encryption(inputFile, format, encryptionMeta, options.password);
82
+ } else {
83
+ await verifyV3Encryption(inputFile, format, metadata, extracted.encryptedContent, encryptionMeta, options.password);
84
+ }
85
+ spinner.succeed('Decryption password valid');
86
+ } catch (e) {
87
+ issues.push('Decryption failed - wrong password or corrupted data');
88
+ spinner.fail('Decryption check failed');
89
+ }
90
+ }
91
+ }
92
+
93
+ // Summary
94
+ console.log();
95
+
96
+ if (issues.length === 0) {
97
+ console.log(chalk.green.bold('✓ File verification passed!'));
98
+ console.log(chalk.cyan(` Original file: ${metadata.originalFilename}`));
99
+ console.log(chalk.cyan(` Size: ${formatBytes(metadata.originalSize)}`));
100
+ console.log(chalk.cyan(` Encrypted: ${isEncrypted ? 'Yes' : 'No'}`));
101
+ console.log(chalk.cyan(` Compressed: ${metadata.compressed ? 'Yes' : 'No'}`));
102
+
103
+ let versionStr = 'v3 (legacy)';
104
+ if (isV5) versionStr = 'v5 (log-embed)';
105
+ else if (isV4) versionStr = 'v4 (streaming)';
106
+ console.log(chalk.cyan(` Format version: ${versionStr}`));
107
+
108
+ if (hasMultipleParts) {
109
+ const totalParts = metadata.totalParts || 'unknown';
110
+ console.log(chalk.cyan(` Parts: ${totalParts}`));
111
+ }
112
+
113
+ console.log();
114
+ console.log(chalk.green('File is ready to decode.'));
115
+ } else {
116
+ console.log(chalk.yellow.bold(' Verification completed with issues:'));
117
+ console.log();
118
+ for (const issue of issues) {
119
+ console.log(chalk.yellow(` • ${issue}`));
120
+ }
121
+ console.log();
122
+
123
+ const hasBlockingIssue = issues.some(i =>
124
+ i.includes('Missing parts') ||
125
+ i.includes('Decryption failed') ||
126
+ i.includes('Metadata')
127
+ );
128
+
129
+ if (hasBlockingIssue) {
130
+ console.log(chalk.red('File cannot be decoded until issues are resolved.'));
131
+ process.exit(1);
132
+ } else {
133
+ console.log(chalk.yellow('File may still be decodable. Run decode to attempt.'));
134
+ }
135
+ }
136
+
137
+ } catch (error) {
138
+ spinner.fail('Verification failed');
139
+ console.error(chalk.red(`Error: ${error.message}`));
140
+ process.exit(1);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Verify v5 log-embed encryption
146
+ */
147
+ async function verifyV5Encryption(extracted, password) {
148
+ const { payloadBuffer, encryptionMeta } = extracted;
149
+ const { iv, salt, authTag } = unpackEncryptionMeta(encryptionMeta);
150
+ const decipher = createDecryptStream(password, iv, salt, authTag);
151
+ decipher.update(payloadBuffer);
152
+ decipher.final();
153
+ }
154
+
155
+ /**
156
+ * Verify v4 per-part encryption
157
+ */
158
+ async function verifyV4Encryption(inputFile, format, encryptionMeta, password) {
159
+ const { iv, salt, authTag } = unpackEncryptionMeta(encryptionMeta);
160
+
161
+ let readResult;
162
+ if (format === 'xlsx') {
163
+ readResult = await readXlsxBase64(inputFile);
164
+ } else {
165
+ readResult = await readDocxBase64(inputFile);
166
+ }
167
+
168
+ const { encryptedContent } = extractContent(readResult, format);
169
+ const binaryData = Buffer.from(encryptedContent, 'base64');
170
+
171
+ const decipher = createDecryptStream(password, iv, salt, authTag);
172
+ decipher.update(binaryData);
173
+ decipher.final();
174
+ }
175
+
176
+ /**
177
+ * Verify v3 shared encryption
178
+ */
179
+ async function verifyV3Encryption(inputFile, format, metadata, encryptedContent, encryptionMeta, password) {
180
+ const { iv, salt, authTag } = unpackEncryptionMeta(encryptionMeta);
181
+
182
+ let fullContent = encryptedContent;
183
+
184
+ if (isMultiPart(metadata)) {
185
+ const inputDir = path.dirname(inputFile);
186
+ const allParts = findMultiPartFiles(inputDir, metadata.hash, format);
187
+
188
+ if (allParts.length === metadata.totalParts) {
189
+ const contentParts = [];
190
+ for (const part of allParts) {
191
+ let partResult;
192
+ if (format === 'xlsx') {
193
+ partResult = await readXlsxBase64(part.path);
194
+ } else {
195
+ partResult = await readDocxBase64(part.path);
196
+ }
197
+ const { encryptedContent: partContent } = extractContent(partResult, format);
198
+ contentParts.push(partContent);
199
+ }
200
+ fullContent = contentParts.join('');
201
+ }
202
+ }
203
+
204
+ decrypt(fullContent, password, iv, salt, authTag);
205
+ }
206
+
207
+ module.exports = verifyCommand;
package/src/index.js CHANGED
@@ -1,87 +1,89 @@
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('CLI tool to encode files into Office documents with AES-256 encryption')
14
- .version('3.0.2');
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);
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('CLI tool to encode files into Office documents with AES-256 encryption')
14
+ .version('5.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('--legacy', 'Use v4 format (hidden sheet + gzip) for backward compatibility')
26
+ .option('--no-limit', 'Bypass DOCX 1 MB size limit (large files will produce huge documents)')
27
+ .option('-q, --quiet', 'Minimal output (for scripting)')
28
+ .option('-y, --yes', 'Skip interactive prompts, use defaults')
29
+ .action(async (file, options) => {
30
+ try {
31
+ await encodeCommand(file, options);
32
+ } catch (error) {
33
+ console.error(chalk.red(`Error: ${error.message}`));
34
+ process.exit(1);
35
+ }
36
+ });
37
+
38
+ // Decode command
39
+ program
40
+ .command('decode <file>')
41
+ .description('Decode and decrypt an XLSX/DOCX file back to original format')
42
+ .option('-o, --output <path>', 'Output file path (defaults to original filename)')
43
+ .option('-p, --password <password>', 'Decryption password (required for encrypted files)')
44
+ .option('--force', 'Overwrite existing files without asking')
45
+ .option('-q, --quiet', 'Minimal output (for scripting)')
46
+ .option('-y, --yes', 'Skip interactive prompts, fail if password needed')
47
+ .action(async (file, options) => {
48
+ try {
49
+ await decodeCommand(file, options);
50
+ } catch (error) {
51
+ console.error(chalk.red(`Error: ${error.message}`));
52
+ process.exit(1);
53
+ }
54
+ });
55
+
56
+ // Info command
57
+ program
58
+ .command('info <file>')
59
+ .description('Show information about an encoded file without decoding')
60
+ .action(async (file, options) => {
61
+ try {
62
+ await infoCommand(file, options);
63
+ } catch (error) {
64
+ console.error(chalk.red(`Error: ${error.message}`));
65
+ process.exit(1);
66
+ }
67
+ });
68
+
69
+ // Verify command
70
+ program
71
+ .command('verify <file>')
72
+ .description('Verify that a file can be decoded without actually decoding')
73
+ .option('-p, --password <password>', 'Password to verify (optional)')
74
+ .action(async (file, options) => {
75
+ try {
76
+ await verifyCommand(file, options);
77
+ } catch (error) {
78
+ console.error(chalk.red(`Error: ${error.message}`));
79
+ process.exit(1);
80
+ }
81
+ });
82
+
83
+ // Show help by default if no command is provided
84
+ if (!process.argv.slice(2).length) {
85
+ program.outputHelp();
86
+ }
87
+
88
+ // Parse arguments
89
+ program.parse(process.argv);