@nightowne/tas-cli 2.1.0 → 2.3.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/README.md +307 -92
- package/package.json +4 -2
- package/src/cli.js +179 -23
- package/src/crypto/encryption.js +25 -3
- package/src/db/index.js +19 -13
- package/src/fuse/mount.js +158 -124
- package/src/index.js +44 -17
- package/src/share/server.js +55 -16
- package/src/sync/sync.js +159 -23
- package/src/telegram/client.js +64 -16
- package/src/utils/branding.js +10 -3
- package/src/utils/chunker.js +4 -2
- package/src/utils/cli-helpers.js +44 -6
- package/src/utils/progress.js +3 -3
package/src/cli.js
CHANGED
|
@@ -15,13 +15,31 @@ import { Compressor } from './utils/compression.js';
|
|
|
15
15
|
import { FileIndex } from './db/index.js';
|
|
16
16
|
import { processFile, retrieveFile } from './index.js';
|
|
17
17
|
import { printBanner, LOGO, TAGLINE, VERSION } from './utils/branding.js';
|
|
18
|
-
import { getPassword, verifyPassword, loadConfig, requireConfig, getAndVerifyPassword } from './utils/cli-helpers.js';
|
|
18
|
+
import { getPassword, verifyPassword, loadConfig, requireConfig, getAndVerifyPassword, decryptBotToken, resolveConfig } from './utils/cli-helpers.js';
|
|
19
19
|
import fs from 'fs';
|
|
20
20
|
import path from 'path';
|
|
21
|
-
import
|
|
21
|
+
import os from 'os';
|
|
22
22
|
|
|
23
|
-
const
|
|
24
|
-
|
|
23
|
+
const DATA_DIR = process.env.TAS_DATA_DIR || path.join(os.homedir(), '.tas');
|
|
24
|
+
|
|
25
|
+
// Global error handlers — prevent silent crashes
|
|
26
|
+
process.on('unhandledRejection', (reason) => {
|
|
27
|
+
console.error(chalk.red('\n✗ Unhandled error:'), reason?.message || reason);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
process.on('uncaughtException', (err) => {
|
|
32
|
+
console.error(chalk.red('\n✗ Fatal error:'), err.message);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Graceful shutdown on signals
|
|
37
|
+
const cleanupAndExit = (signal) => {
|
|
38
|
+
console.log(chalk.dim(`\n${signal} received, shutting down...`));
|
|
39
|
+
process.exit(0);
|
|
40
|
+
};
|
|
41
|
+
process.on('SIGINT', () => cleanupAndExit('SIGINT'));
|
|
42
|
+
process.on('SIGTERM', () => cleanupAndExit('SIGTERM'));
|
|
25
43
|
|
|
26
44
|
const program = new Command();
|
|
27
45
|
|
|
@@ -104,14 +122,16 @@ program
|
|
|
104
122
|
const userInfo = await client.waitForChatId(120000);
|
|
105
123
|
spinner.succeed(`Linked to ${userInfo.firstName} (@${userInfo.username})`);
|
|
106
124
|
|
|
107
|
-
// Save config
|
|
125
|
+
// Save config (bot token encrypted with user's password)
|
|
108
126
|
const configPath = path.join(DATA_DIR, 'config.json');
|
|
127
|
+
const encryptedToken = encryptor.encrypt(Buffer.from(token, 'utf-8')).toString('base64');
|
|
109
128
|
fs.writeFileSync(configPath, JSON.stringify({
|
|
110
|
-
|
|
129
|
+
encryptedBotToken: encryptedToken,
|
|
111
130
|
chatId: userInfo.chatId,
|
|
112
131
|
passwordHash: encryptor.getPasswordHash(),
|
|
113
132
|
username: userInfo.username,
|
|
114
|
-
createdAt: new Date().toISOString()
|
|
133
|
+
createdAt: new Date().toISOString(),
|
|
134
|
+
configVersion: 2
|
|
115
135
|
}, null, 2));
|
|
116
136
|
|
|
117
137
|
// Initialize database
|
|
@@ -152,11 +172,12 @@ program
|
|
|
152
172
|
process.exit(1);
|
|
153
173
|
}
|
|
154
174
|
|
|
155
|
-
const
|
|
175
|
+
const rawConfig = requireConfig(DATA_DIR);
|
|
156
176
|
spinner.stop();
|
|
157
177
|
|
|
158
178
|
// Get and verify password
|
|
159
179
|
const password = await getAndVerifyPassword(options.password, DATA_DIR);
|
|
180
|
+
const config = resolveConfig(rawConfig, password);
|
|
160
181
|
|
|
161
182
|
spinner.start('Processing file...');
|
|
162
183
|
|
|
@@ -207,7 +228,7 @@ program
|
|
|
207
228
|
const spinner = ora('Looking up file...').start();
|
|
208
229
|
|
|
209
230
|
try {
|
|
210
|
-
const
|
|
231
|
+
const rawConfig = requireConfig(DATA_DIR);
|
|
211
232
|
|
|
212
233
|
// Find file in index
|
|
213
234
|
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
@@ -223,6 +244,7 @@ program
|
|
|
223
244
|
|
|
224
245
|
// Get and verify password
|
|
225
246
|
const password = await getAndVerifyPassword(options.password, DATA_DIR);
|
|
247
|
+
const config = resolveConfig(rawConfig, password);
|
|
226
248
|
|
|
227
249
|
spinner.start('Downloading...');
|
|
228
250
|
|
|
@@ -266,6 +288,7 @@ program
|
|
|
266
288
|
.alias('ls')
|
|
267
289
|
.description('List all stored files')
|
|
268
290
|
.option('-l, --long', 'Show detailed information')
|
|
291
|
+
.option('--json', 'Output as JSON (for scripting)')
|
|
269
292
|
.action(async (options) => {
|
|
270
293
|
try {
|
|
271
294
|
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
@@ -273,8 +296,15 @@ program
|
|
|
273
296
|
|
|
274
297
|
const files = db.listAll();
|
|
275
298
|
|
|
299
|
+
if (options.json) {
|
|
300
|
+
console.log(JSON.stringify(files, null, 2));
|
|
301
|
+
db.close();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
276
305
|
if (files.length === 0) {
|
|
277
306
|
console.log(chalk.yellow('\n📭 No files stored yet. Use `tas push <file>` to upload.\n'));
|
|
307
|
+
db.close();
|
|
278
308
|
return;
|
|
279
309
|
}
|
|
280
310
|
|
|
@@ -298,6 +328,7 @@ program
|
|
|
298
328
|
}
|
|
299
329
|
|
|
300
330
|
console.log();
|
|
331
|
+
db.close();
|
|
301
332
|
|
|
302
333
|
} catch (err) {
|
|
303
334
|
console.error(chalk.red('Error listing files:'), err.message);
|
|
@@ -311,6 +342,7 @@ program
|
|
|
311
342
|
.alias('rm')
|
|
312
343
|
.description('Remove a file from the index (optionally from Telegram too)')
|
|
313
344
|
.option('--hard', 'Also delete from Telegram')
|
|
345
|
+
.option('-p, --password <password>', 'Encryption password (required for --hard)')
|
|
314
346
|
.action(async (identifier, options) => {
|
|
315
347
|
try {
|
|
316
348
|
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
@@ -334,8 +366,9 @@ program
|
|
|
334
366
|
if (confirm) {
|
|
335
367
|
// If hard delete, also remove from Telegram
|
|
336
368
|
if (options.hard) {
|
|
337
|
-
const
|
|
338
|
-
const
|
|
369
|
+
const rawConfig = requireConfig(DATA_DIR);
|
|
370
|
+
const password = await getAndVerifyPassword(options.password, DATA_DIR);
|
|
371
|
+
const config = resolveConfig(rawConfig, password);
|
|
339
372
|
|
|
340
373
|
const client = new TelegramClient(DATA_DIR);
|
|
341
374
|
await client.initialize(config.botToken);
|
|
@@ -361,11 +394,16 @@ program
|
|
|
361
394
|
program
|
|
362
395
|
.command('status')
|
|
363
396
|
.description('Show TAS status and statistics')
|
|
364
|
-
.
|
|
397
|
+
.option('--json', 'Output as JSON (for scripting)')
|
|
398
|
+
.action(async (options) => {
|
|
365
399
|
const configPath = path.join(DATA_DIR, 'config.json');
|
|
366
400
|
|
|
367
401
|
if (!fs.existsSync(configPath)) {
|
|
368
|
-
|
|
402
|
+
if (options.json) {
|
|
403
|
+
console.log(JSON.stringify({ initialized: false }));
|
|
404
|
+
} else {
|
|
405
|
+
console.log(chalk.yellow('\n⚠️ TAS not initialized. Run `tas init` first.\n'));
|
|
406
|
+
}
|
|
369
407
|
return;
|
|
370
408
|
}
|
|
371
409
|
|
|
@@ -378,13 +416,30 @@ program
|
|
|
378
416
|
const storedSize = files.reduce((acc, f) => acc + f.stored_size, 0);
|
|
379
417
|
const savings = totalSize > 0 ? Math.round((1 - storedSize / totalSize) * 100) : 0;
|
|
380
418
|
|
|
419
|
+
if (options.json) {
|
|
420
|
+
console.log(JSON.stringify({
|
|
421
|
+
initialized: true,
|
|
422
|
+
createdAt: config.createdAt,
|
|
423
|
+
username: config.username || 'unknown',
|
|
424
|
+
fileCount: files.length,
|
|
425
|
+
totalSize,
|
|
426
|
+
storedSize,
|
|
427
|
+
savingsPercent: savings,
|
|
428
|
+
dataDir: DATA_DIR
|
|
429
|
+
}, null, 2));
|
|
430
|
+
db.close();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
381
434
|
console.log(chalk.cyan('\n📊 TAS Status\n'));
|
|
382
435
|
console.log(` Initialized: ${chalk.white(new Date(config.createdAt).toLocaleDateString())}`);
|
|
383
436
|
console.log(` Telegram user: ${chalk.white('@' + (config.username || 'unknown'))}`);
|
|
437
|
+
console.log(` Data dir: ${chalk.white(DATA_DIR)}`);
|
|
384
438
|
console.log(` Files stored: ${chalk.white(files.length)}`);
|
|
385
439
|
console.log(` Total size: ${chalk.white(formatBytes(totalSize))}`);
|
|
386
440
|
console.log(` Compressed: ${chalk.white(formatBytes(storedSize))} ${chalk.dim(`(${savings}% saved)`)}`);
|
|
387
441
|
console.log();
|
|
442
|
+
db.close();
|
|
388
443
|
});
|
|
389
444
|
|
|
390
445
|
// ============== MOUNT COMMAND ==============
|
|
@@ -395,8 +450,9 @@ program
|
|
|
395
450
|
.action(async (mountpoint, options) => {
|
|
396
451
|
console.log(chalk.cyan('\n🗂️ Mounting Telegram as filesystem...\n'));
|
|
397
452
|
|
|
398
|
-
const
|
|
453
|
+
const rawConfig = requireConfig(DATA_DIR);
|
|
399
454
|
const password = await getAndVerifyPassword(options.password, DATA_DIR);
|
|
455
|
+
const config = resolveConfig(rawConfig, password);
|
|
400
456
|
|
|
401
457
|
const spinner = ora('Initializing filesystem...').start();
|
|
402
458
|
|
|
@@ -678,8 +734,9 @@ syncCmd
|
|
|
678
734
|
.action(async (options) => {
|
|
679
735
|
console.log(chalk.cyan('\n🔄 Starting folder sync...\n'));
|
|
680
736
|
|
|
681
|
-
const
|
|
737
|
+
const rawConfig = requireConfig(DATA_DIR);
|
|
682
738
|
const password = await getAndVerifyPassword(options.password, DATA_DIR);
|
|
739
|
+
const config = resolveConfig(rawConfig, password);
|
|
683
740
|
|
|
684
741
|
let limitRate = null;
|
|
685
742
|
if (options.limit) {
|
|
@@ -768,8 +825,9 @@ syncCmd
|
|
|
768
825
|
.action(async (options) => {
|
|
769
826
|
console.log(chalk.cyan('\n📥 Pulling files from Telegram...\n'));
|
|
770
827
|
|
|
771
|
-
const
|
|
828
|
+
const rawConfig = requireConfig(DATA_DIR);
|
|
772
829
|
const password = await getAndVerifyPassword(options.password, DATA_DIR);
|
|
830
|
+
const config = resolveConfig(rawConfig, password);
|
|
773
831
|
|
|
774
832
|
const spinner = ora('Loading...').start();
|
|
775
833
|
|
|
@@ -854,10 +912,13 @@ syncCmd
|
|
|
854
912
|
program
|
|
855
913
|
.command('verify')
|
|
856
914
|
.description('Verify file integrity and check for missing Telegram messages')
|
|
857
|
-
.
|
|
915
|
+
.option('-p, --password <password>', 'Encryption password')
|
|
916
|
+
.action(async (options) => {
|
|
858
917
|
console.log(chalk.cyan('\n🔍 Verifying file integrity...\n'));
|
|
859
918
|
|
|
860
|
-
const
|
|
919
|
+
const rawConfig = requireConfig(DATA_DIR);
|
|
920
|
+
const password = await getAndVerifyPassword(options.password, DATA_DIR);
|
|
921
|
+
const config = resolveConfig(rawConfig, password);
|
|
861
922
|
|
|
862
923
|
const spinner = ora('Checking files...').start();
|
|
863
924
|
|
|
@@ -944,10 +1005,11 @@ program
|
|
|
944
1005
|
|
|
945
1006
|
// Helper function
|
|
946
1007
|
function formatBytes(bytes) {
|
|
1008
|
+
if (!Number.isFinite(bytes) || bytes < 0) return '0 B';
|
|
947
1009
|
if (bytes === 0) return '0 B';
|
|
948
1010
|
const k = 1024;
|
|
949
|
-
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
950
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1011
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
1012
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
|
|
951
1013
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
952
1014
|
}
|
|
953
1015
|
|
|
@@ -1052,8 +1114,8 @@ program
|
|
|
1052
1114
|
}
|
|
1053
1115
|
|
|
1054
1116
|
// Resume uploads
|
|
1055
|
-
const
|
|
1056
|
-
if (!
|
|
1117
|
+
const rawConfig = loadConfig(DATA_DIR);
|
|
1118
|
+
if (!rawConfig) {
|
|
1057
1119
|
console.log(chalk.red('✗ TAS not initialized.'));
|
|
1058
1120
|
db.close();
|
|
1059
1121
|
return;
|
|
@@ -1061,6 +1123,7 @@ program
|
|
|
1061
1123
|
|
|
1062
1124
|
// Get and verify password
|
|
1063
1125
|
const password = await getAndVerifyPassword(options.password, DATA_DIR);
|
|
1126
|
+
const config = resolveConfig(rawConfig, password);
|
|
1064
1127
|
|
|
1065
1128
|
// Connect to Telegram
|
|
1066
1129
|
const { TelegramClient } = await import('./telegram/client.js');
|
|
@@ -1147,8 +1210,9 @@ shareCmd
|
|
|
1147
1210
|
.action(async (file, options) => {
|
|
1148
1211
|
console.log(chalk.cyan('\n🔗 Creating share link...\n'));
|
|
1149
1212
|
|
|
1150
|
-
const
|
|
1213
|
+
const rawConfig = requireConfig(DATA_DIR);
|
|
1151
1214
|
const password = await getAndVerifyPassword(options.password, DATA_DIR);
|
|
1215
|
+
const config = resolveConfig(rawConfig, password);
|
|
1152
1216
|
|
|
1153
1217
|
const spinner = ora('Setting up...').start();
|
|
1154
1218
|
|
|
@@ -1299,6 +1363,98 @@ shareCmd
|
|
|
1299
1363
|
}
|
|
1300
1364
|
});
|
|
1301
1365
|
|
|
1366
|
+
// ============== DOCTOR COMMAND ==============
|
|
1367
|
+
program
|
|
1368
|
+
.command('doctor')
|
|
1369
|
+
.description('🩺 Run self-diagnostics and check system health')
|
|
1370
|
+
.action(async () => {
|
|
1371
|
+
console.log(chalk.cyan('\n🩺 TAS Doctor — System Health Check\n'));
|
|
1372
|
+
|
|
1373
|
+
const checks = [];
|
|
1374
|
+
const ok = (label) => { checks.push({ label, status: 'ok' }); console.log(chalk.green(` ✓ ${label}`)); };
|
|
1375
|
+
const warn = (label, detail) => { checks.push({ label, status: 'warn', detail }); console.log(chalk.yellow(` ⚠ ${label}`) + chalk.dim(` — ${detail}`)); };
|
|
1376
|
+
const fail = (label, detail) => { checks.push({ label, status: 'fail', detail }); console.log(chalk.red(` ✗ ${label}`) + chalk.dim(` — ${detail}`)); };
|
|
1377
|
+
|
|
1378
|
+
// 1. Check Node.js version
|
|
1379
|
+
const nodeVer = process.versions.node;
|
|
1380
|
+
const major = parseInt(nodeVer.split('.')[0]);
|
|
1381
|
+
if (major >= 18) ok(`Node.js ${nodeVer}`);
|
|
1382
|
+
else warn(`Node.js ${nodeVer}`, 'Requires >= 18.0.0');
|
|
1383
|
+
|
|
1384
|
+
// 2. Check data directory
|
|
1385
|
+
if (fs.existsSync(DATA_DIR)) ok(`Data directory: ${DATA_DIR}`);
|
|
1386
|
+
else warn('Data directory missing', `Run \`tas init\` to create ${DATA_DIR}`);
|
|
1387
|
+
|
|
1388
|
+
// 3. Check config
|
|
1389
|
+
const configPath = path.join(DATA_DIR, 'config.json');
|
|
1390
|
+
if (fs.existsSync(configPath)) {
|
|
1391
|
+
try {
|
|
1392
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1393
|
+
if (config.configVersion === 2) ok('Config v2 (encrypted token)');
|
|
1394
|
+
else if (config.botToken) warn('Config v1 (plaintext token)', 'Re-run `tas init` to encrypt token');
|
|
1395
|
+
else fail('Config invalid', 'Missing bot token');
|
|
1396
|
+
|
|
1397
|
+
if (config.chatId) ok(`Chat ID: ${config.chatId}`);
|
|
1398
|
+
else fail('Chat ID missing', 'Run `tas init`');
|
|
1399
|
+
} catch (e) {
|
|
1400
|
+
fail('Config corrupted', e.message);
|
|
1401
|
+
}
|
|
1402
|
+
} else {
|
|
1403
|
+
warn('Config not found', 'Run `tas init`');
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// 4. Check database
|
|
1407
|
+
const dbPath = path.join(DATA_DIR, 'index.db');
|
|
1408
|
+
if (fs.existsSync(dbPath)) {
|
|
1409
|
+
try {
|
|
1410
|
+
const db = new FileIndex(dbPath);
|
|
1411
|
+
db.init();
|
|
1412
|
+
const stats = db.getStats();
|
|
1413
|
+
ok(`Database: ${stats.file_count} files, ${formatBytes(stats.total_original)} total`);
|
|
1414
|
+
db.close();
|
|
1415
|
+
} catch (e) {
|
|
1416
|
+
fail('Database error', e.message);
|
|
1417
|
+
}
|
|
1418
|
+
} else {
|
|
1419
|
+
warn('Database not found', 'Will be created on first upload');
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// 5. Check FUSE availability
|
|
1423
|
+
try {
|
|
1424
|
+
await import('fuse-native');
|
|
1425
|
+
ok('FUSE support available');
|
|
1426
|
+
} catch (e) {
|
|
1427
|
+
warn('FUSE not available', 'Install libfuse for mount support');
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// 6. Check disk space
|
|
1431
|
+
try {
|
|
1432
|
+
const { execSync } = await import('child_process');
|
|
1433
|
+
const df = execSync(`df -h "${DATA_DIR}" 2>/dev/null || echo "unknown"`).toString().trim();
|
|
1434
|
+
const lines = df.split('\n');
|
|
1435
|
+
if (lines.length > 1) {
|
|
1436
|
+
const parts = lines[1].split(/\s+/);
|
|
1437
|
+
const avail = parts[3] || 'unknown';
|
|
1438
|
+
const usage = parts[4] || 'unknown';
|
|
1439
|
+
if (parseInt(usage) > 90) warn(`Disk space: ${avail} free (${usage} used)`, 'Running low!');
|
|
1440
|
+
else ok(`Disk space: ${avail} free (${usage} used)`);
|
|
1441
|
+
}
|
|
1442
|
+
} catch (e) { /* ignore */ }
|
|
1443
|
+
|
|
1444
|
+
// 7. Security check
|
|
1445
|
+
const iterations = 600000;
|
|
1446
|
+
ok(`Encryption: AES-256-GCM, PBKDF2-SHA512 ${iterations.toLocaleString()} iterations`);
|
|
1447
|
+
|
|
1448
|
+
// Summary
|
|
1449
|
+
const fails = checks.filter(c => c.status === 'fail').length;
|
|
1450
|
+
const warns = checks.filter(c => c.status === 'warn').length;
|
|
1451
|
+
console.log();
|
|
1452
|
+
if (fails > 0) console.log(chalk.red(` ${fails} issue(s) found. Please fix them above.`));
|
|
1453
|
+
else if (warns > 0) console.log(chalk.yellow(` ${warns} warning(s). System is functional.`));
|
|
1454
|
+
else console.log(chalk.green(' ✨ All systems go! TAS is healthy.'));
|
|
1455
|
+
console.log();
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1302
1458
|
program.parse();
|
|
1303
1459
|
|
|
1304
1460
|
|
package/src/crypto/encryption.js
CHANGED
|
@@ -10,7 +10,7 @@ const KEY_LENGTH = 32; // 256 bits
|
|
|
10
10
|
const IV_LENGTH = 12; // 96 bits for GCM
|
|
11
11
|
const TAG_LENGTH = 16; // 128 bits auth tag
|
|
12
12
|
const SALT_LENGTH = 32;
|
|
13
|
-
const PBKDF2_ITERATIONS =
|
|
13
|
+
const PBKDF2_ITERATIONS = 600000; // OWASP 2025 recommendation for SHA-512
|
|
14
14
|
|
|
15
15
|
export class Encryptor {
|
|
16
16
|
constructor(password) {
|
|
@@ -19,11 +19,29 @@ export class Encryptor {
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Get a hash of the password for verification (not the actual key!)
|
|
22
|
+
* Uses PBKDF2 with a fixed salt derived from the password domain to make brute-force expensive.
|
|
22
23
|
*/
|
|
23
24
|
getPasswordHash() {
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const verifySalt = Buffer.from('tas-password-verify-v2', 'utf-8');
|
|
26
|
+
return crypto.pbkdf2Sync(this.password, verifySalt, PBKDF2_ITERATIONS, 32, 'sha512').toString('hex');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check password against a stored hash (supports both legacy SHA-256 and new PBKDF2 formats)
|
|
31
|
+
*/
|
|
32
|
+
static verifyPasswordHash(password, storedHash) {
|
|
33
|
+
const encryptor = new Encryptor(password);
|
|
34
|
+
|
|
35
|
+
// Try new PBKDF2-based verification first
|
|
36
|
+
if (encryptor.getPasswordHash() === storedHash) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fallback: legacy SHA-256 verification for backward compatibility
|
|
41
|
+
const legacyHash = crypto.createHash('sha256')
|
|
42
|
+
.update(password + 'was-verify')
|
|
26
43
|
.digest('hex');
|
|
44
|
+
return legacyHash === storedHash;
|
|
27
45
|
}
|
|
28
46
|
|
|
29
47
|
/**
|
|
@@ -92,6 +110,10 @@ export class Encryptor {
|
|
|
92
110
|
callback();
|
|
93
111
|
},
|
|
94
112
|
flush(callback) {
|
|
113
|
+
if (!headerWritten) {
|
|
114
|
+
this.push(Buffer.concat([salt, iv]));
|
|
115
|
+
headerWritten = true;
|
|
116
|
+
}
|
|
95
117
|
const final = cipher.final();
|
|
96
118
|
if (final.length > 0) {
|
|
97
119
|
this.push(final);
|
package/src/db/index.js
CHANGED
|
@@ -171,25 +171,32 @@ export class FileIndex {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
|
-
*
|
|
174
|
+
* Escape SQL LIKE wildcard characters
|
|
175
|
+
*/
|
|
176
|
+
_escapeLike(str) {
|
|
177
|
+
return str.replace(/[%_\\]/g, '\\$&');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Find file by hash (exact match or prefix match)
|
|
175
182
|
*/
|
|
176
183
|
findByHash(hash) {
|
|
177
184
|
const stmt = this.db.prepare(`
|
|
178
|
-
SELECT * FROM files WHERE hash = ? OR hash LIKE ?
|
|
185
|
+
SELECT * FROM files WHERE hash = ? OR hash LIKE ? ESCAPE '\\'
|
|
179
186
|
`);
|
|
180
187
|
|
|
181
|
-
return stmt.get(hash, hash + '%');
|
|
188
|
+
return stmt.get(hash, this._escapeLike(hash) + '%');
|
|
182
189
|
}
|
|
183
190
|
|
|
184
191
|
/**
|
|
185
|
-
* Find file by filename
|
|
192
|
+
* Find file by filename (exact match or substring match)
|
|
186
193
|
*/
|
|
187
194
|
findByName(filename) {
|
|
188
195
|
const stmt = this.db.prepare(`
|
|
189
|
-
SELECT * FROM files WHERE filename = ? OR filename LIKE ?
|
|
196
|
+
SELECT * FROM files WHERE filename = ? OR filename LIKE ? ESCAPE '\\'
|
|
190
197
|
`);
|
|
191
198
|
|
|
192
|
-
return stmt.get(filename, '%' + filename + '%');
|
|
199
|
+
return stmt.get(filename, '%' + this._escapeLike(filename) + '%');
|
|
193
200
|
}
|
|
194
201
|
|
|
195
202
|
/**
|
|
@@ -399,11 +406,11 @@ export class FileIndex {
|
|
|
399
406
|
SELECT f.*, GROUP_CONCAT(t.tag) as tags
|
|
400
407
|
FROM files f
|
|
401
408
|
LEFT JOIN tags t ON f.id = t.file_id
|
|
402
|
-
WHERE f.filename LIKE ?
|
|
409
|
+
WHERE f.filename LIKE ? ESCAPE '\\'
|
|
403
410
|
GROUP BY f.id
|
|
404
411
|
ORDER BY f.created_at DESC
|
|
405
412
|
`);
|
|
406
|
-
return stmt.all(`%${query}%`);
|
|
413
|
+
return stmt.all(`%${this._escapeLike(query)}%`);
|
|
407
414
|
}
|
|
408
415
|
|
|
409
416
|
/**
|
|
@@ -414,11 +421,11 @@ export class FileIndex {
|
|
|
414
421
|
SELECT f.*, GROUP_CONCAT(t.tag) as tags
|
|
415
422
|
FROM files f
|
|
416
423
|
INNER JOIN tags t ON f.id = t.file_id
|
|
417
|
-
WHERE t.tag LIKE ?
|
|
424
|
+
WHERE t.tag LIKE ? ESCAPE '\\'
|
|
418
425
|
GROUP BY f.id
|
|
419
426
|
ORDER BY f.created_at DESC
|
|
420
427
|
`);
|
|
421
|
-
return stmt.all(`%${query}%`);
|
|
428
|
+
return stmt.all(`%${this._escapeLike(query)}%`);
|
|
422
429
|
}
|
|
423
430
|
|
|
424
431
|
// ============== RESUME UPLOAD METHODS ==============
|
|
@@ -572,10 +579,9 @@ export class FileIndex {
|
|
|
572
579
|
*/
|
|
573
580
|
cleanExpiredShares() {
|
|
574
581
|
const stmt = this.db.prepare(`
|
|
575
|
-
DELETE FROM shares WHERE
|
|
576
|
-
REPLACE(REPLACE(expires_at, 'T', ' '), 'Z', '') < strftime('%Y-%m-%d %H:%M:%f', 'now')
|
|
582
|
+
DELETE FROM shares WHERE expires_at < ?
|
|
577
583
|
`);
|
|
578
|
-
return stmt.run().changes;
|
|
584
|
+
return stmt.run(new Date().toISOString()).changes;
|
|
579
585
|
}
|
|
580
586
|
|
|
581
587
|
/**
|