stegdoc 4.0.0 → 5.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/LICENSE +21 -21
- package/README.md +214 -214
- package/package.json +59 -59
- package/src/commands/decode.js +485 -343
- package/src/commands/encode.js +567 -449
- package/src/commands/info.js +118 -114
- package/src/commands/verify.js +207 -204
- package/src/index.js +89 -87
- package/src/lib/compression.js +177 -115
- package/src/lib/crypto.js +172 -172
- package/src/lib/decoy-generator.js +306 -306
- package/src/lib/docx-handler.js +587 -161
- package/src/lib/docx-templates.js +355 -0
- package/src/lib/file-handler.js +113 -113
- package/src/lib/file-utils.js +160 -150
- package/src/lib/interactive.js +190 -190
- package/src/lib/log-generator.js +764 -0
- package/src/lib/metadata.js +151 -122
- package/src/lib/streams.js +197 -197
- package/src/lib/utils.js +227 -227
- package/src/lib/xlsx-handler.js +597 -416
- package/src/lib/xml-utils.js +115 -115
package/src/lib/interactive.js
CHANGED
|
@@ -1,190 +1,190 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interactive prompts for encode/decode commands
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Check if running in interactive mode (no relevant flags provided)
|
|
7
|
-
* @param {object} options - Command options
|
|
8
|
-
* @param {string} command - Command name ('encode' or 'decode')
|
|
9
|
-
* @returns {boolean}
|
|
10
|
-
*/
|
|
11
|
-
function shouldRunInteractive(options, command) {
|
|
12
|
-
// If --yes flag is set, never run interactive
|
|
13
|
-
if (options.yes) return false;
|
|
14
|
-
|
|
15
|
-
// If --quiet flag is set, never run interactive
|
|
16
|
-
if (options.quiet) return false;
|
|
17
|
-
|
|
18
|
-
if (command === 'encode') {
|
|
19
|
-
// Run interactive if no format or password specified
|
|
20
|
-
const hasFormat = options.format && options.format !== 'xlsx'; // xlsx is default
|
|
21
|
-
const hasPassword = !!options.password;
|
|
22
|
-
return !hasFormat && !hasPassword;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (command === 'decode') {
|
|
26
|
-
// For decode, only prompt if encrypted file needs password
|
|
27
|
-
// This is handled separately in decode command
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Prompt for encode options interactively
|
|
36
|
-
* @param {string} filename - Input filename for context
|
|
37
|
-
* @returns {Promise<object>} Selected options
|
|
38
|
-
*/
|
|
39
|
-
async function promptEncodeOptions(filename) {
|
|
40
|
-
// Dynamic import for ESM inquirer
|
|
41
|
-
const { default: inquirer } = await import('inquirer');
|
|
42
|
-
|
|
43
|
-
console.log();
|
|
44
|
-
|
|
45
|
-
// First get format and encryption choice
|
|
46
|
-
const basicAnswers = await inquirer.prompt([
|
|
47
|
-
{
|
|
48
|
-
type: 'list',
|
|
49
|
-
name: 'format',
|
|
50
|
-
message: 'Select output format:',
|
|
51
|
-
choices: [
|
|
52
|
-
{ name: 'XLSX (Excel) - recommended,
|
|
53
|
-
{ name: 'DOCX (Word) -
|
|
54
|
-
],
|
|
55
|
-
default: 'xlsx',
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
type: 'confirm',
|
|
59
|
-
name: 'useEncryption',
|
|
60
|
-
message: 'Encrypt the file with a password?',
|
|
61
|
-
default: true,
|
|
62
|
-
},
|
|
63
|
-
]);
|
|
64
|
-
|
|
65
|
-
let password;
|
|
66
|
-
|
|
67
|
-
// If encryption is enabled, prompt for password with confirmation
|
|
68
|
-
if (basicAnswers.useEncryption) {
|
|
69
|
-
let passwordsMatch = false;
|
|
70
|
-
|
|
71
|
-
while (!passwordsMatch) {
|
|
72
|
-
const { password: pwd } = await inquirer.prompt([
|
|
73
|
-
{
|
|
74
|
-
type: 'password',
|
|
75
|
-
name: 'password',
|
|
76
|
-
message: 'Enter encryption password:',
|
|
77
|
-
mask: '*',
|
|
78
|
-
validate: (input) => {
|
|
79
|
-
if (input.length < 8) {
|
|
80
|
-
return 'Password must be at least 8 characters';
|
|
81
|
-
}
|
|
82
|
-
return true;
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
]);
|
|
86
|
-
|
|
87
|
-
const { passwordConfirm } = await inquirer.prompt([
|
|
88
|
-
{
|
|
89
|
-
type: 'password',
|
|
90
|
-
name: 'passwordConfirm',
|
|
91
|
-
message: 'Confirm password:',
|
|
92
|
-
mask: '*',
|
|
93
|
-
},
|
|
94
|
-
]);
|
|
95
|
-
|
|
96
|
-
if (pwd === passwordConfirm) {
|
|
97
|
-
password = pwd;
|
|
98
|
-
passwordsMatch = true;
|
|
99
|
-
} else {
|
|
100
|
-
console.log('Passwords do not match. Please try again.\n');
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Get chunk size
|
|
106
|
-
const { chunkSize } = await inquirer.prompt([
|
|
107
|
-
{
|
|
108
|
-
type: 'input',
|
|
109
|
-
name: 'chunkSize',
|
|
110
|
-
message: 'Chunk size or number of parts (e.g., 5MB, "3 parts", or "max"):',
|
|
111
|
-
default: '5MB',
|
|
112
|
-
validate: (input) => {
|
|
113
|
-
const trimmed = input.trim().toLowerCase();
|
|
114
|
-
// Allow special values for no splitting
|
|
115
|
-
if (['0', 'max', 'single', 'none'].includes(trimmed)) {
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
118
|
-
// Allow "X parts" format
|
|
119
|
-
if (/^\d+\s*parts?$/i.test(trimmed)) {
|
|
120
|
-
const num = parseInt(trimmed, 10);
|
|
121
|
-
if (num < 1) return 'Number of parts must be at least 1';
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
// Allow size format
|
|
125
|
-
if (!/^\d+(\.\d+)?\s*(B|KB|MB|GB)?$/i.test(trimmed)) {
|
|
126
|
-
return 'Use "5MB", "3 parts", or "max" for no splitting';
|
|
127
|
-
}
|
|
128
|
-
return true;
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
]);
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
format: basicAnswers.format,
|
|
135
|
-
password,
|
|
136
|
-
chunkSize,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Prompt for password when decoding encrypted file
|
|
142
|
-
* @returns {Promise<string>} Password
|
|
143
|
-
*/
|
|
144
|
-
async function promptPassword() {
|
|
145
|
-
const { default: inquirer } = await import('inquirer');
|
|
146
|
-
|
|
147
|
-
const { password } = await inquirer.prompt([
|
|
148
|
-
{
|
|
149
|
-
type: 'password',
|
|
150
|
-
name: 'password',
|
|
151
|
-
message: 'Enter decryption password:',
|
|
152
|
-
mask: '*',
|
|
153
|
-
validate: (input) => {
|
|
154
|
-
if (!input) {
|
|
155
|
-
return 'Password is required for encrypted files';
|
|
156
|
-
}
|
|
157
|
-
return true;
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
]);
|
|
161
|
-
|
|
162
|
-
return password;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Prompt for confirmation before overwriting
|
|
167
|
-
* @param {string} filePath - Path to file that will be overwritten
|
|
168
|
-
* @returns {Promise<boolean>} True if user confirms
|
|
169
|
-
*/
|
|
170
|
-
async function promptOverwrite(filePath) {
|
|
171
|
-
const { default: inquirer } = await import('inquirer');
|
|
172
|
-
|
|
173
|
-
const { confirm } = await inquirer.prompt([
|
|
174
|
-
{
|
|
175
|
-
type: 'confirm',
|
|
176
|
-
name: 'confirm',
|
|
177
|
-
message: `File "${filePath}" already exists. Overwrite?`,
|
|
178
|
-
default: false,
|
|
179
|
-
},
|
|
180
|
-
]);
|
|
181
|
-
|
|
182
|
-
return confirm;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
module.exports = {
|
|
186
|
-
shouldRunInteractive,
|
|
187
|
-
promptEncodeOptions,
|
|
188
|
-
promptPassword,
|
|
189
|
-
promptOverwrite,
|
|
190
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Interactive prompts for encode/decode commands
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if running in interactive mode (no relevant flags provided)
|
|
7
|
+
* @param {object} options - Command options
|
|
8
|
+
* @param {string} command - Command name ('encode' or 'decode')
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
function shouldRunInteractive(options, command) {
|
|
12
|
+
// If --yes flag is set, never run interactive
|
|
13
|
+
if (options.yes) return false;
|
|
14
|
+
|
|
15
|
+
// If --quiet flag is set, never run interactive
|
|
16
|
+
if (options.quiet) return false;
|
|
17
|
+
|
|
18
|
+
if (command === 'encode') {
|
|
19
|
+
// Run interactive if no format or password specified
|
|
20
|
+
const hasFormat = options.format && options.format !== 'xlsx'; // xlsx is default
|
|
21
|
+
const hasPassword = !!options.password;
|
|
22
|
+
return !hasFormat && !hasPassword;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (command === 'decode') {
|
|
26
|
+
// For decode, only prompt if encrypted file needs password
|
|
27
|
+
// This is handled separately in decode command
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Prompt for encode options interactively
|
|
36
|
+
* @param {string} filename - Input filename for context
|
|
37
|
+
* @returns {Promise<object>} Selected options
|
|
38
|
+
*/
|
|
39
|
+
async function promptEncodeOptions(filename) {
|
|
40
|
+
// Dynamic import for ESM inquirer
|
|
41
|
+
const { default: inquirer } = await import('inquirer');
|
|
42
|
+
|
|
43
|
+
console.log();
|
|
44
|
+
|
|
45
|
+
// First get format and encryption choice
|
|
46
|
+
const basicAnswers = await inquirer.prompt([
|
|
47
|
+
{
|
|
48
|
+
type: 'list',
|
|
49
|
+
name: 'format',
|
|
50
|
+
message: 'Select output format:',
|
|
51
|
+
choices: [
|
|
52
|
+
{ name: 'XLSX (Excel) - recommended, any file size', value: 'xlsx' },
|
|
53
|
+
{ name: 'DOCX (Word) - files under 1 MB only', value: 'docx' },
|
|
54
|
+
],
|
|
55
|
+
default: 'xlsx',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'confirm',
|
|
59
|
+
name: 'useEncryption',
|
|
60
|
+
message: 'Encrypt the file with a password?',
|
|
61
|
+
default: true,
|
|
62
|
+
},
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
let password;
|
|
66
|
+
|
|
67
|
+
// If encryption is enabled, prompt for password with confirmation
|
|
68
|
+
if (basicAnswers.useEncryption) {
|
|
69
|
+
let passwordsMatch = false;
|
|
70
|
+
|
|
71
|
+
while (!passwordsMatch) {
|
|
72
|
+
const { password: pwd } = await inquirer.prompt([
|
|
73
|
+
{
|
|
74
|
+
type: 'password',
|
|
75
|
+
name: 'password',
|
|
76
|
+
message: 'Enter encryption password:',
|
|
77
|
+
mask: '*',
|
|
78
|
+
validate: (input) => {
|
|
79
|
+
if (input.length < 8) {
|
|
80
|
+
return 'Password must be at least 8 characters';
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const { passwordConfirm } = await inquirer.prompt([
|
|
88
|
+
{
|
|
89
|
+
type: 'password',
|
|
90
|
+
name: 'passwordConfirm',
|
|
91
|
+
message: 'Confirm password:',
|
|
92
|
+
mask: '*',
|
|
93
|
+
},
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
if (pwd === passwordConfirm) {
|
|
97
|
+
password = pwd;
|
|
98
|
+
passwordsMatch = true;
|
|
99
|
+
} else {
|
|
100
|
+
console.log('Passwords do not match. Please try again.\n');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Get chunk size
|
|
106
|
+
const { chunkSize } = await inquirer.prompt([
|
|
107
|
+
{
|
|
108
|
+
type: 'input',
|
|
109
|
+
name: 'chunkSize',
|
|
110
|
+
message: 'Chunk size or number of parts (e.g., 5MB, "3 parts", or "max"):',
|
|
111
|
+
default: '5MB',
|
|
112
|
+
validate: (input) => {
|
|
113
|
+
const trimmed = input.trim().toLowerCase();
|
|
114
|
+
// Allow special values for no splitting
|
|
115
|
+
if (['0', 'max', 'single', 'none'].includes(trimmed)) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
// Allow "X parts" format
|
|
119
|
+
if (/^\d+\s*parts?$/i.test(trimmed)) {
|
|
120
|
+
const num = parseInt(trimmed, 10);
|
|
121
|
+
if (num < 1) return 'Number of parts must be at least 1';
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
// Allow size format
|
|
125
|
+
if (!/^\d+(\.\d+)?\s*(B|KB|MB|GB)?$/i.test(trimmed)) {
|
|
126
|
+
return 'Use "5MB", "3 parts", or "max" for no splitting';
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
format: basicAnswers.format,
|
|
135
|
+
password,
|
|
136
|
+
chunkSize,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Prompt for password when decoding encrypted file
|
|
142
|
+
* @returns {Promise<string>} Password
|
|
143
|
+
*/
|
|
144
|
+
async function promptPassword() {
|
|
145
|
+
const { default: inquirer } = await import('inquirer');
|
|
146
|
+
|
|
147
|
+
const { password } = await inquirer.prompt([
|
|
148
|
+
{
|
|
149
|
+
type: 'password',
|
|
150
|
+
name: 'password',
|
|
151
|
+
message: 'Enter decryption password:',
|
|
152
|
+
mask: '*',
|
|
153
|
+
validate: (input) => {
|
|
154
|
+
if (!input) {
|
|
155
|
+
return 'Password is required for encrypted files';
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
return password;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Prompt for confirmation before overwriting
|
|
167
|
+
* @param {string} filePath - Path to file that will be overwritten
|
|
168
|
+
* @returns {Promise<boolean>} True if user confirms
|
|
169
|
+
*/
|
|
170
|
+
async function promptOverwrite(filePath) {
|
|
171
|
+
const { default: inquirer } = await import('inquirer');
|
|
172
|
+
|
|
173
|
+
const { confirm } = await inquirer.prompt([
|
|
174
|
+
{
|
|
175
|
+
type: 'confirm',
|
|
176
|
+
name: 'confirm',
|
|
177
|
+
message: `File "${filePath}" already exists. Overwrite?`,
|
|
178
|
+
default: false,
|
|
179
|
+
},
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
return confirm;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
shouldRunInteractive,
|
|
187
|
+
promptEncodeOptions,
|
|
188
|
+
promptPassword,
|
|
189
|
+
promptOverwrite,
|
|
190
|
+
};
|