-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathencrypt.js
More file actions
247 lines (225 loc) · 8.51 KB
/
Copy pathencrypt.js
File metadata and controls
247 lines (225 loc) · 8.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
#!/usr/bin/env node
'use strict';
/*!
* StaticShield CLI 入口
*
* 用法:node encrypt.js <文件...> [选项] | node encrypt.js --gui
*
* 流程:解析参数 → 获取密码(交互/参数)→ 逐文件读取、提取 title/favicon、
* 加密、组装自解密页面、写出;可选生成分享链接。
*/
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const crypto = require('./src/crypto-core');
const { buildEncryptedHtml } = require('./src/template');
const { extractLogo, extractTitle, fileToDataUri } = require('./src/favicon');
const { bundleHtml } = require('./src/bundle');
const { generatePassword } = require('./src/password');
const { parseArgs, formatHelp } = require('./src/cli-args');
// ---- 交互式密码输入(不回显,支持多字节 UTF-8)----
function promptPassword() {
const stdin = process.stdin;
const interactive = stdin.isTTY && typeof stdin.setRawMode === 'function';
process.stderr.write('请输入加密密码: ');
return new Promise((resolve) => {
if (!interactive) {
// 非交互(管道/重定向):读取一行
let buf = '';
stdin.resume();
stdin.setEncoding('utf8');
const onData = (d) => {
const i = d.indexOf('\n');
if (i >= 0) {
buf += d.slice(0, i).replace(/\r$/, '');
cleanup();
process.stderr.write('\n');
resolve(buf);
} else {
buf += d;
}
};
// EOF 兜底:管道输入无尾换行(printf / < /dev/null)时也 resolve,避免静默退出
const onEnd = () => {
cleanup();
process.stderr.write('\n');
resolve(buf);
};
const cleanup = () => {
stdin.removeListener('data', onData);
stdin.removeListener('end', onEnd);
stdin.destroy();
};
stdin.on('data', onData);
stdin.on('end', onEnd);
return;
}
// 交互模式:raw 模式按字节收集,回显 *,结束时按 UTF-8 解码(兼容中文密码)
const bytes = [];
stdin.setRawMode(true);
stdin.resume();
const finish = () => {
stdin.setRawMode(false);
stdin.pause();
stdin.removeListener('data', onData);
process.stderr.write('\n');
resolve(Buffer.from(bytes).toString('utf8'));
};
const onData = (chunk) => {
for (let k = 0; k < chunk.length; k++) {
const b = chunk[k];
if (b === 0x0d || b === 0x0a) { // Enter
return finish();
} else if (b === 0x03) { // Ctrl-C
process.stderr.write('\n');
process.exit(0);
} else if (b === 0x08 || b === 0x7f) { // Backspace / Delete:删除整个 UTF-8 字符(兼容中文/emoji 密码)
if (bytes.length) {
let charBytes = 1;
while (bytes.length - charBytes > 0 && (bytes[bytes.length - 1 - charBytes] & 0xC0) === 0x80) charBytes++;
for (let j = 0; j < charBytes; j++) {
bytes.pop();
process.stderr.write('\b \b');
}
}
} else if (b >= 0x20 && b !== 0x7f) {
bytes.push(b);
process.stderr.write('*');
}
}
};
stdin.on('data', onData);
});
}
// ---- 启动本地 GUI 后端服务并打开浏览器(支持多客户端并行)----
function openUrl(url) {
// 数组传参、不走 shell,避免特殊字符引发命令注入
if (process.platform === 'win32') spawnSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });
else if (process.platform === 'darwin') spawnSync('open', [url], { stdio: 'ignore' });
else spawnSync('xdg-open', [url], { stdio: 'ignore' });
}
async function startGui() {
const guiPath = path.join(__dirname, 'gui', 'index.html');
if (!fs.existsSync(guiPath)) {
console.error('未找到 Web GUI(gui/index.html)。请先运行:node src/build-gui.js');
process.exit(1);
}
const { startServer } = require('./src/server');
let info;
try {
info = await startServer({ guiPath });
} catch (e) {
console.error('启动本地服务失败:' + e.message);
process.exit(1);
}
const url = 'http://' + info.host + ':' + info.port + '/';
console.log('StaticShield GUI 已启动:' + url);
console.log('(多个客户端可同时访问该地址进行并行加密;按 Ctrl+C 停止服务)');
openUrl(url);
}
// ---- 处理单个文件 ----
async function processFile(file, password, args) {
if (!fs.existsSync(file)) throw new Error('文件不存在: ' + file);
const baseDir = path.dirname(path.resolve(file));
let html = fs.readFileSync(file, 'utf8');
// 多文件网页:先把本地 css/js/图片内联成自包含单 HTML,再走加密(解密端无需改动)
if (args.bundle) {
const stats = {};
html = bundleHtml(html, baseDir, stats);
const total = stats.css + stats.js + stats.img + stats.icon + stats.import;
console.log(' 打包:内联 ' + total + ' 个资源(css ' + stats.css + ' / js ' + stats.js +
' / 图片 ' + stats.img + ' / favicon ' + stats.icon + ' / @import ' + stats.import + ')');
}
const title = extractTitle(html);
// Logo:--logo 优先;否则从网页提取 favicon(bundle 后 icon 已为 data URI)
let logo = '';
if (args.logo) {
if (!fs.existsSync(args.logo)) throw new Error('Logo 文件不存在: ' + args.logo);
logo = fileToDataUri(args.logo);
} else {
logo = await extractLogo(html, baseDir);
}
const meta = await crypto.encryptHtml(html, password, { useSha512: args.sha512 });
meta.hint = args.hint || '';
meta.title = title || '';
meta.logo = logo || '';
if (args.remember !== null && args.remember !== undefined) meta.rememberDays = args.remember;
return meta;
}
// ---- 主流程 ----
async function main() {
let args;
try {
args = parseArgs(process.argv.slice(2));
} catch (e) {
console.error('错误:' + e.message);
console.error('使用 --help 查看用法。');
process.exit(1);
}
if (args.help) {
console.log(formatHelp());
return;
}
if (args.gui) {
await startGui();
return;
}
// 仅生成随机密码(不带文件)
if (args.genPwd && args.files.length === 0) {
console.log(generatePassword(args.genPwd));
return;
}
if (args.files.length === 0) {
console.error('错误:请指定要加密的 HTML 文件。使用 --help 查看用法。');
process.exit(1);
}
// 密码:--gen-pwd 生成随机密码;否则用 -p 或交互输入
let password = args.password;
if (args.genPwd) {
password = generatePassword(args.genPwd);
console.log('已生成随机密码(' + args.genPwd + ' 位):' + password);
console.log('⚠ 请妥善保存此密码,丢失后无法恢复内容。');
}
if (!password) password = await promptPassword();
if (!password) {
console.error('错误:密码不能为空。');
process.exit(1);
}
// 输出目录
if (args.directory && args.directory !== '.' && !fs.existsSync(args.directory)) {
fs.mkdirSync(args.directory, { recursive: true });
}
let failed = 0;
for (const file of args.files) {
try {
const meta = await processFile(file, password, args);
const outHtml = buildEncryptedHtml(meta);
let outPath = path.join(args.directory, path.basename(file));
// 防止默认输出覆盖源文件(原始明文不可恢复丢失)
if (path.resolve(outPath) === path.resolve(file)) {
const base = path.basename(file);
let newName = base.replace(/(\.html?)$/i, '.encrypted$1');
if (newName === base) newName = base + '.encrypted.html';
outPath = path.join(args.directory, newName);
console.warn('⚠ 检测到输出路径与源文件相同,已重命名为 ' + newName + ' 以避免覆盖原始文件');
}
fs.writeFileSync(outPath, outHtml);
console.log('✓ 已加密:' + file + ' → ' + outPath);
if (args.share) {
const link = 'https://<托管地址>/' + encodeURIComponent(path.basename(file)) +
'#pwd=' + encodeURIComponent(password);
console.log(' 分享链接(密码置于 URL hash,不会随请求发送到服务器):');
console.log(' ' + link);
console.log(' 注意:链接本身即包含密码,请仅在可信范围按需分享。');
}
} catch (e) {
console.error('✗ 加密失败:' + file + ' — ' + e.message);
failed++;
}
}
process.exitCode = failed ? 1 : 0;
}
main().catch((e) => {
console.error('错误:' + (e && e.message ? e.message : e));
process.exit(1);
});