@nightowne/tas-cli 1.1.1 → 2.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.
- package/README.md +102 -97
- package/package.json +3 -3
- package/src/cli.js +212 -147
- package/src/crypto/encryption.js +128 -0
- package/src/db/index.js +86 -0
- package/src/fuse/mount.js +104 -40
- package/src/index.js +173 -103
- package/src/share/server.js +441 -0
- package/src/sync/sync.js +54 -35
- package/src/telegram/client.js +45 -17
- package/src/utils/chunker.js +11 -1
- package/src/utils/cli-helpers.js +145 -0
- package/src/utils/compression.js +30 -0
- package/src/utils/throttle.js +26 -0
package/src/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ 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
19
|
import fs from 'fs';
|
|
19
20
|
import path from 'path';
|
|
20
21
|
import { fileURLToPath } from 'url';
|
|
@@ -140,6 +141,7 @@ program
|
|
|
140
141
|
.command('push <file>')
|
|
141
142
|
.description('Upload a file to Telegram storage')
|
|
142
143
|
.option('-n, --name <name>', 'Custom name for the file')
|
|
144
|
+
.option('-p, --password <password>', 'Encryption password (uses TAS_PASSWORD env var if not provided)')
|
|
143
145
|
.action(async (file, options) => {
|
|
144
146
|
const spinner = ora('Preparing...').start();
|
|
145
147
|
|
|
@@ -150,32 +152,11 @@ program
|
|
|
150
152
|
process.exit(1);
|
|
151
153
|
}
|
|
152
154
|
|
|
153
|
-
|
|
154
|
-
const configPath = path.join(DATA_DIR, 'config.json');
|
|
155
|
-
if (!fs.existsSync(configPath)) {
|
|
156
|
-
spinner.fail('TAS not initialized. Run `tas init` first.');
|
|
157
|
-
process.exit(1);
|
|
158
|
-
}
|
|
159
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
160
|
-
|
|
155
|
+
const config = requireConfig(DATA_DIR);
|
|
161
156
|
spinner.stop();
|
|
162
157
|
|
|
163
|
-
// Get password
|
|
164
|
-
const
|
|
165
|
-
{
|
|
166
|
-
type: 'password',
|
|
167
|
-
name: 'password',
|
|
168
|
-
message: 'Enter your encryption password:',
|
|
169
|
-
mask: '*'
|
|
170
|
-
}
|
|
171
|
-
]);
|
|
172
|
-
|
|
173
|
-
// Verify password
|
|
174
|
-
const encryptor = new Encryptor(password);
|
|
175
|
-
if (encryptor.getPasswordHash() !== config.passwordHash) {
|
|
176
|
-
console.log(chalk.red('✗ Incorrect password'));
|
|
177
|
-
process.exit(1);
|
|
178
|
-
}
|
|
158
|
+
// Get and verify password
|
|
159
|
+
const password = await getAndVerifyPassword(options.password, DATA_DIR);
|
|
179
160
|
|
|
180
161
|
spinner.start('Processing file...');
|
|
181
162
|
|
|
@@ -221,17 +202,12 @@ program
|
|
|
221
202
|
.command('pull <identifier> [output]')
|
|
222
203
|
.description('Download a file from Telegram storage (by filename or hash)')
|
|
223
204
|
.option('-o, --output <path>', 'Output path for the file')
|
|
205
|
+
.option('-p, --password <password>', 'Encryption password (uses TAS_PASSWORD env var if not provided)')
|
|
224
206
|
.action(async (identifier, output, options) => {
|
|
225
207
|
const spinner = ora('Looking up file...').start();
|
|
226
208
|
|
|
227
209
|
try {
|
|
228
|
-
|
|
229
|
-
const configPath = path.join(DATA_DIR, 'config.json');
|
|
230
|
-
if (!fs.existsSync(configPath)) {
|
|
231
|
-
spinner.fail('TAS not initialized. Run `tas init` first.');
|
|
232
|
-
process.exit(1);
|
|
233
|
-
}
|
|
234
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
210
|
+
const config = requireConfig(DATA_DIR);
|
|
235
211
|
|
|
236
212
|
// Find file in index
|
|
237
213
|
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
@@ -245,22 +221,8 @@ program
|
|
|
245
221
|
|
|
246
222
|
spinner.stop();
|
|
247
223
|
|
|
248
|
-
// Get password
|
|
249
|
-
const
|
|
250
|
-
{
|
|
251
|
-
type: 'password',
|
|
252
|
-
name: 'password',
|
|
253
|
-
message: 'Enter your encryption password:',
|
|
254
|
-
mask: '*'
|
|
255
|
-
}
|
|
256
|
-
]);
|
|
257
|
-
|
|
258
|
-
// Verify password
|
|
259
|
-
const encryptor = new Encryptor(password);
|
|
260
|
-
if (encryptor.getPasswordHash() !== config.passwordHash) {
|
|
261
|
-
console.log(chalk.red('✗ Incorrect password'));
|
|
262
|
-
process.exit(1);
|
|
263
|
-
}
|
|
224
|
+
// Get and verify password
|
|
225
|
+
const password = await getAndVerifyPassword(options.password, DATA_DIR);
|
|
264
226
|
|
|
265
227
|
spinner.start('Downloading...');
|
|
266
228
|
|
|
@@ -429,33 +391,12 @@ program
|
|
|
429
391
|
program
|
|
430
392
|
.command('mount <mountpoint>')
|
|
431
393
|
.description('🔥 Mount Telegram storage as a local folder (FUSE)')
|
|
432
|
-
.
|
|
394
|
+
.option('-p, --password <password>', 'Encryption password (uses TAS_PASSWORD env var if not provided)')
|
|
395
|
+
.action(async (mountpoint, options) => {
|
|
433
396
|
console.log(chalk.cyan('\n🗂️ Mounting Telegram as filesystem...\n'));
|
|
434
397
|
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
if (!fs.existsSync(configPath)) {
|
|
438
|
-
console.log(chalk.red('✗ TAS not initialized. Run `tas init` first.'));
|
|
439
|
-
process.exit(1);
|
|
440
|
-
}
|
|
441
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
442
|
-
|
|
443
|
-
// Get password
|
|
444
|
-
const { password } = await inquirer.prompt([
|
|
445
|
-
{
|
|
446
|
-
type: 'password',
|
|
447
|
-
name: 'password',
|
|
448
|
-
message: 'Enter your encryption password:',
|
|
449
|
-
mask: '*'
|
|
450
|
-
}
|
|
451
|
-
]);
|
|
452
|
-
|
|
453
|
-
// Verify password
|
|
454
|
-
const encryptor = new Encryptor(password);
|
|
455
|
-
if (encryptor.getPasswordHash() !== config.passwordHash) {
|
|
456
|
-
console.log(chalk.red('✗ Incorrect password'));
|
|
457
|
-
process.exit(1);
|
|
458
|
-
}
|
|
398
|
+
const config = requireConfig(DATA_DIR);
|
|
399
|
+
const password = await getAndVerifyPassword(options.password, DATA_DIR);
|
|
459
400
|
|
|
460
401
|
const spinner = ora('Initializing filesystem...').start();
|
|
461
402
|
|
|
@@ -732,32 +673,29 @@ syncCmd
|
|
|
732
673
|
syncCmd
|
|
733
674
|
.command('start')
|
|
734
675
|
.description('Start syncing all registered folders')
|
|
735
|
-
.
|
|
676
|
+
.option('-p, --password <password>', 'Encryption password (uses TAS_PASSWORD env var if not provided)')
|
|
677
|
+
.option('-l, --limit <limit>', 'Bandwidth limit (e.g. 500k, 1m)')
|
|
678
|
+
.action(async (options) => {
|
|
736
679
|
console.log(chalk.cyan('\n🔄 Starting folder sync...\n'));
|
|
737
680
|
|
|
738
|
-
|
|
739
|
-
const
|
|
740
|
-
if (!fs.existsSync(configPath)) {
|
|
741
|
-
console.log(chalk.red('✗ TAS not initialized. Run `tas init` first.'));
|
|
742
|
-
process.exit(1);
|
|
743
|
-
}
|
|
744
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
681
|
+
const config = requireConfig(DATA_DIR);
|
|
682
|
+
const password = await getAndVerifyPassword(options.password, DATA_DIR);
|
|
745
683
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
mask: '*'
|
|
684
|
+
let limitRate = null;
|
|
685
|
+
if (options.limit) {
|
|
686
|
+
const match = options.limit.match(/^(\d+)([kmg]?)$/i);
|
|
687
|
+
if (!match) {
|
|
688
|
+
console.error(chalk.red('Invalid limit format. Use e.g. 500{}, 1m'));
|
|
689
|
+
process.exit(1);
|
|
753
690
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
691
|
+
const val = parseInt(match[1]);
|
|
692
|
+
const unit = match[2].toLowerCase();
|
|
693
|
+
if (unit === 'k') limitRate = val * 1024;
|
|
694
|
+
else if (unit === 'm') limitRate = val * 1024 * 1024;
|
|
695
|
+
else if (unit === 'g') limitRate = val * 1024 * 1024 * 1024;
|
|
696
|
+
else limitRate = val;
|
|
697
|
+
|
|
698
|
+
console.log(chalk.dim(` Bandwidth limit: ${options.limit}/s`));
|
|
761
699
|
}
|
|
762
700
|
|
|
763
701
|
try {
|
|
@@ -766,7 +704,8 @@ syncCmd
|
|
|
766
704
|
const syncEngine = new SyncEngine({
|
|
767
705
|
dataDir: DATA_DIR,
|
|
768
706
|
password,
|
|
769
|
-
config
|
|
707
|
+
config,
|
|
708
|
+
limitRate
|
|
770
709
|
});
|
|
771
710
|
|
|
772
711
|
await syncEngine.initialize();
|
|
@@ -825,33 +764,12 @@ syncCmd
|
|
|
825
764
|
syncCmd
|
|
826
765
|
.command('pull')
|
|
827
766
|
.description('Download all Telegram files to sync folders (two-way sync)')
|
|
828
|
-
.
|
|
767
|
+
.option('-p, --password <password>', 'Encryption password (uses TAS_PASSWORD env var if not provided)')
|
|
768
|
+
.action(async (options) => {
|
|
829
769
|
console.log(chalk.cyan('\n📥 Pulling files from Telegram...\n'));
|
|
830
770
|
|
|
831
|
-
|
|
832
|
-
const
|
|
833
|
-
if (!fs.existsSync(configPath)) {
|
|
834
|
-
console.log(chalk.red('✗ TAS not initialized. Run `tas init` first.'));
|
|
835
|
-
process.exit(1);
|
|
836
|
-
}
|
|
837
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
838
|
-
|
|
839
|
-
// Get password
|
|
840
|
-
const { password } = await inquirer.prompt([
|
|
841
|
-
{
|
|
842
|
-
type: 'password',
|
|
843
|
-
name: 'password',
|
|
844
|
-
message: 'Enter your encryption password:',
|
|
845
|
-
mask: '*'
|
|
846
|
-
}
|
|
847
|
-
]);
|
|
848
|
-
|
|
849
|
-
// Verify password
|
|
850
|
-
const encryptor = new Encryptor(password);
|
|
851
|
-
if (encryptor.getPasswordHash() !== config.passwordHash) {
|
|
852
|
-
console.log(chalk.red('✗ Incorrect password'));
|
|
853
|
-
process.exit(1);
|
|
854
|
-
}
|
|
771
|
+
const config = requireConfig(DATA_DIR);
|
|
772
|
+
const password = await getAndVerifyPassword(options.password, DATA_DIR);
|
|
855
773
|
|
|
856
774
|
const spinner = ora('Loading...').start();
|
|
857
775
|
|
|
@@ -939,13 +857,7 @@ program
|
|
|
939
857
|
.action(async () => {
|
|
940
858
|
console.log(chalk.cyan('\n🔍 Verifying file integrity...\n'));
|
|
941
859
|
|
|
942
|
-
|
|
943
|
-
const configPath = path.join(DATA_DIR, 'config.json');
|
|
944
|
-
if (!fs.existsSync(configPath)) {
|
|
945
|
-
console.log(chalk.red('✗ TAS not initialized. Run `tas init` first.'));
|
|
946
|
-
process.exit(1);
|
|
947
|
-
}
|
|
948
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
860
|
+
const config = requireConfig(DATA_DIR);
|
|
949
861
|
|
|
950
862
|
const spinner = ora('Checking files...').start();
|
|
951
863
|
|
|
@@ -1078,7 +990,8 @@ program
|
|
|
1078
990
|
program
|
|
1079
991
|
.command('resume')
|
|
1080
992
|
.description('Resume interrupted uploads')
|
|
1081
|
-
.
|
|
993
|
+
.option('-p, --password <password>', 'Encryption password (uses TAS_PASSWORD env var if not provided)')
|
|
994
|
+
.action(async (options) => {
|
|
1082
995
|
try {
|
|
1083
996
|
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
1084
997
|
db.init();
|
|
@@ -1139,30 +1052,15 @@ program
|
|
|
1139
1052
|
}
|
|
1140
1053
|
|
|
1141
1054
|
// Resume uploads
|
|
1142
|
-
const
|
|
1143
|
-
if (!
|
|
1055
|
+
const config = loadConfig(DATA_DIR);
|
|
1056
|
+
if (!config) {
|
|
1144
1057
|
console.log(chalk.red('✗ TAS not initialized.'));
|
|
1145
1058
|
db.close();
|
|
1146
1059
|
return;
|
|
1147
1060
|
}
|
|
1148
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1149
|
-
|
|
1150
|
-
// Get password
|
|
1151
|
-
const { password } = await inquirer.prompt([
|
|
1152
|
-
{
|
|
1153
|
-
type: 'password',
|
|
1154
|
-
name: 'password',
|
|
1155
|
-
message: 'Enter your encryption password:',
|
|
1156
|
-
mask: '*'
|
|
1157
|
-
}
|
|
1158
|
-
]);
|
|
1159
1061
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
console.log(chalk.red('✗ Incorrect password'));
|
|
1163
|
-
db.close();
|
|
1164
|
-
return;
|
|
1165
|
-
}
|
|
1062
|
+
// Get and verify password
|
|
1063
|
+
const password = await getAndVerifyPassword(options.password, DATA_DIR);
|
|
1166
1064
|
|
|
1167
1065
|
// Connect to Telegram
|
|
1168
1066
|
const { TelegramClient } = await import('./telegram/client.js');
|
|
@@ -1234,6 +1132,173 @@ program
|
|
|
1234
1132
|
}
|
|
1235
1133
|
});
|
|
1236
1134
|
|
|
1135
|
+
// ============== SHARE COMMAND ==============
|
|
1136
|
+
const shareCmd = program
|
|
1137
|
+
.command('share')
|
|
1138
|
+
.description('🔗 Share files via temporary download links');
|
|
1139
|
+
|
|
1140
|
+
shareCmd
|
|
1141
|
+
.command('create <file>')
|
|
1142
|
+
.description('Create a temporary download link for a file')
|
|
1143
|
+
.option('-e, --expire <duration>', 'Expiry duration (e.g. 1h, 24h, 7d)', '24h')
|
|
1144
|
+
.option('-m, --max-downloads <n>', 'Maximum number of downloads', '1')
|
|
1145
|
+
.option('--port <port>', 'HTTP server port', '3000')
|
|
1146
|
+
.option('-p, --password <password>', 'Encryption password')
|
|
1147
|
+
.action(async (file, options) => {
|
|
1148
|
+
console.log(chalk.cyan('\n🔗 Creating share link...\n'));
|
|
1149
|
+
|
|
1150
|
+
const config = requireConfig(DATA_DIR);
|
|
1151
|
+
const password = await getAndVerifyPassword(options.password, DATA_DIR);
|
|
1152
|
+
|
|
1153
|
+
const spinner = ora('Setting up...').start();
|
|
1154
|
+
|
|
1155
|
+
try {
|
|
1156
|
+
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
1157
|
+
db.init();
|
|
1158
|
+
|
|
1159
|
+
const fileRecord = db.findByHash(file) || db.findByName(file);
|
|
1160
|
+
if (!fileRecord) {
|
|
1161
|
+
spinner.fail(`File not found: ${file}`);
|
|
1162
|
+
process.exit(1);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Generate token and calculate expiry
|
|
1166
|
+
const { generateToken, parseDuration } = await import('./share/server.js');
|
|
1167
|
+
const token = generateToken();
|
|
1168
|
+
const expiresAt = new Date(Date.now() + parseDuration(options.expire)).toISOString();
|
|
1169
|
+
const maxDownloads = parseInt(options.maxDownloads) || 1;
|
|
1170
|
+
|
|
1171
|
+
// Add share to DB
|
|
1172
|
+
db.addShare(fileRecord.id, token, expiresAt, maxDownloads);
|
|
1173
|
+
|
|
1174
|
+
// Start share server
|
|
1175
|
+
const { ShareServer } = await import('./share/server.js');
|
|
1176
|
+
const port = parseInt(options.port) || 3000;
|
|
1177
|
+
|
|
1178
|
+
const server = new ShareServer({
|
|
1179
|
+
dataDir: DATA_DIR,
|
|
1180
|
+
password,
|
|
1181
|
+
config,
|
|
1182
|
+
port
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
await server.initialize();
|
|
1186
|
+
await server.start();
|
|
1187
|
+
|
|
1188
|
+
spinner.succeed('Share server running!');
|
|
1189
|
+
|
|
1190
|
+
// Get local IP for network sharing
|
|
1191
|
+
const { networkInterfaces } = await import('os');
|
|
1192
|
+
const nets = networkInterfaces();
|
|
1193
|
+
let localIP = 'localhost';
|
|
1194
|
+
for (const name of Object.keys(nets)) {
|
|
1195
|
+
for (const net of nets[name]) {
|
|
1196
|
+
if (net.family === 'IPv4' && !net.internal) {
|
|
1197
|
+
localIP = net.address;
|
|
1198
|
+
break;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
console.log(chalk.cyan('\n📎 Share Links:\n'));
|
|
1204
|
+
console.log(` ${chalk.white('Local:')} ${chalk.green(`http://localhost:${port}/d/${token}`)}`);
|
|
1205
|
+
console.log(` ${chalk.white('Network:')} ${chalk.green(`http://${localIP}:${port}/d/${token}`)}`);
|
|
1206
|
+
console.log();
|
|
1207
|
+
console.log(chalk.dim(` File: ${fileRecord.filename}`));
|
|
1208
|
+
console.log(chalk.dim(` Expires: ${options.expire}`));
|
|
1209
|
+
console.log(chalk.dim(` Downloads: ${maxDownloads} max`));
|
|
1210
|
+
console.log(chalk.dim(` Token: ${token.substring(0, 8)}...`));
|
|
1211
|
+
console.log();
|
|
1212
|
+
console.log(chalk.yellow('Press Ctrl+C to stop the share server'));
|
|
1213
|
+
|
|
1214
|
+
// Handle graceful shutdown
|
|
1215
|
+
const cleanup = async () => {
|
|
1216
|
+
console.log(chalk.dim('\n\nStopping share server...'));
|
|
1217
|
+
await server.stop();
|
|
1218
|
+
console.log(chalk.green('✓ Share server stopped'));
|
|
1219
|
+
process.exit(0);
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
process.on('SIGINT', cleanup);
|
|
1223
|
+
process.on('SIGTERM', cleanup);
|
|
1224
|
+
|
|
1225
|
+
// Keep process running
|
|
1226
|
+
await new Promise(() => { });
|
|
1227
|
+
|
|
1228
|
+
} catch (err) {
|
|
1229
|
+
spinner.fail(`Share failed: ${err.message}`);
|
|
1230
|
+
process.exit(1);
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
shareCmd
|
|
1235
|
+
.command('list')
|
|
1236
|
+
.description('List all active share links')
|
|
1237
|
+
.action(async () => {
|
|
1238
|
+
try {
|
|
1239
|
+
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
1240
|
+
db.init();
|
|
1241
|
+
|
|
1242
|
+
// Clean expired first
|
|
1243
|
+
const cleaned = db.cleanExpiredShares();
|
|
1244
|
+
if (cleaned > 0) {
|
|
1245
|
+
console.log(chalk.dim(` (${cleaned} expired shares cleaned)`));
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const shares = db.listShares();
|
|
1249
|
+
|
|
1250
|
+
if (shares.length === 0) {
|
|
1251
|
+
console.log(chalk.yellow('\n📭 No active shares. Use `tas share create <file>` to create one.\n'));
|
|
1252
|
+
} else {
|
|
1253
|
+
console.log(chalk.cyan(`\n🔗 Active Shares (${shares.length})\n`));
|
|
1254
|
+
|
|
1255
|
+
for (const share of shares) {
|
|
1256
|
+
const expired = new Date(share.expires_at) < new Date();
|
|
1257
|
+
const status = expired
|
|
1258
|
+
? chalk.red('expired')
|
|
1259
|
+
: chalk.green('active');
|
|
1260
|
+
|
|
1261
|
+
console.log(` ${chalk.blue('●')} ${share.filename}`);
|
|
1262
|
+
console.log(chalk.dim(` Token: ${share.token.substring(0, 8)}... Status: ${status} Downloads: ${share.download_count}/${share.max_downloads}`));
|
|
1263
|
+
console.log(chalk.dim(` Expires: ${new Date(share.expires_at).toLocaleString()}`));
|
|
1264
|
+
}
|
|
1265
|
+
console.log();
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
db.close();
|
|
1269
|
+
} catch (err) {
|
|
1270
|
+
console.error(chalk.red('Error:'), err.message);
|
|
1271
|
+
process.exit(1);
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
shareCmd
|
|
1276
|
+
.command('revoke <token>')
|
|
1277
|
+
.description('Revoke a share link')
|
|
1278
|
+
.action(async (token) => {
|
|
1279
|
+
try {
|
|
1280
|
+
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
1281
|
+
db.init();
|
|
1282
|
+
|
|
1283
|
+
// Support partial token match
|
|
1284
|
+
const shares = db.listShares();
|
|
1285
|
+
const match = shares.find(s => s.token === token || s.token.startsWith(token));
|
|
1286
|
+
|
|
1287
|
+
if (!match) {
|
|
1288
|
+
console.log(chalk.red(`✗ Share not found: ${token}`));
|
|
1289
|
+
process.exit(1);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
db.revokeShare(match.token);
|
|
1293
|
+
console.log(chalk.green(`✓ Revoked share for "${match.filename}"`));
|
|
1294
|
+
|
|
1295
|
+
db.close();
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
console.error(chalk.red('Error:'), err.message);
|
|
1298
|
+
process.exit(1);
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1237
1302
|
program.parse();
|
|
1238
1303
|
|
|
1239
1304
|
|
package/src/crypto/encryption.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import crypto from 'crypto';
|
|
6
|
+
import { Transform } from 'stream';
|
|
6
7
|
|
|
7
8
|
const ALGORITHM = 'aes-256-gcm';
|
|
8
9
|
const KEY_LENGTH = 32; // 256 bits
|
|
@@ -66,6 +67,41 @@ export class Encryptor {
|
|
|
66
67
|
return Buffer.concat([salt, iv, encrypted, authTag]);
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Get an encryption transform stream
|
|
72
|
+
* Needs to append the salt/iv to the stream begin, and authTag to the stream end
|
|
73
|
+
*/
|
|
74
|
+
getEncryptStream() {
|
|
75
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
76
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
77
|
+
const key = this.deriveKey(salt);
|
|
78
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
79
|
+
|
|
80
|
+
let headerWritten = false;
|
|
81
|
+
|
|
82
|
+
return new Transform({
|
|
83
|
+
transform(chunk, encoding, callback) {
|
|
84
|
+
if (!headerWritten) {
|
|
85
|
+
this.push(Buffer.concat([salt, iv]));
|
|
86
|
+
headerWritten = true;
|
|
87
|
+
}
|
|
88
|
+
const encrypted = cipher.update(chunk);
|
|
89
|
+
if (encrypted.length > 0) {
|
|
90
|
+
this.push(encrypted);
|
|
91
|
+
}
|
|
92
|
+
callback();
|
|
93
|
+
},
|
|
94
|
+
flush(callback) {
|
|
95
|
+
const final = cipher.final();
|
|
96
|
+
if (final.length > 0) {
|
|
97
|
+
this.push(final);
|
|
98
|
+
}
|
|
99
|
+
this.push(cipher.getAuthTag());
|
|
100
|
+
callback();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
69
105
|
/**
|
|
70
106
|
* Decrypt data
|
|
71
107
|
* Input: Buffer containing [salt (32) | iv (12) | ciphertext | authTag (16)]
|
|
@@ -90,6 +126,98 @@ export class Encryptor {
|
|
|
90
126
|
decipher.final()
|
|
91
127
|
]);
|
|
92
128
|
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get a decryption transform stream
|
|
132
|
+
* Expects [salt (32) | iv (12) | ciphertext | authTag (16)]
|
|
133
|
+
*/
|
|
134
|
+
getDecryptStream() {
|
|
135
|
+
let salt = null;
|
|
136
|
+
let iv = null;
|
|
137
|
+
let authTag = null;
|
|
138
|
+
let key = null;
|
|
139
|
+
let decipher = null;
|
|
140
|
+
|
|
141
|
+
// Buffer for storing the salt and iv during the first few chunks
|
|
142
|
+
let headerBuffer = Buffer.alloc(0);
|
|
143
|
+
let headerRead = false;
|
|
144
|
+
|
|
145
|
+
// We must buffer the last 16 bytes across chunks because it's the authTag
|
|
146
|
+
let tailBuffer = Buffer.alloc(0);
|
|
147
|
+
|
|
148
|
+
const self = this;
|
|
149
|
+
|
|
150
|
+
return new Transform({
|
|
151
|
+
transform(chunk, encoding, callback) {
|
|
152
|
+
try {
|
|
153
|
+
// 1. Read the header (salt + iv)
|
|
154
|
+
if (!headerRead) {
|
|
155
|
+
headerBuffer = Buffer.concat([headerBuffer, chunk]);
|
|
156
|
+
|
|
157
|
+
if (headerBuffer.length >= SALT_LENGTH + IV_LENGTH) {
|
|
158
|
+
salt = headerBuffer.subarray(0, SALT_LENGTH);
|
|
159
|
+
iv = headerBuffer.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
|
|
160
|
+
key = self.deriveKey(salt);
|
|
161
|
+
decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
162
|
+
|
|
163
|
+
// The rest of the header buffer is ciphertext
|
|
164
|
+
const remaining = headerBuffer.subarray(SALT_LENGTH + IV_LENGTH);
|
|
165
|
+
headerBuffer = null; // free memory
|
|
166
|
+
headerRead = true;
|
|
167
|
+
|
|
168
|
+
// Push remaining into tailBuffer for processing
|
|
169
|
+
if (remaining.length > 0) {
|
|
170
|
+
tailBuffer = Buffer.concat([tailBuffer, remaining]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
tailBuffer = Buffer.concat([tailBuffer, chunk]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 2. Process ciphertext, keeping exactly TAG_LENGTH bytes in tailBuffer
|
|
178
|
+
if (headerRead && tailBuffer.length > TAG_LENGTH) {
|
|
179
|
+
const processLength = tailBuffer.length - TAG_LENGTH;
|
|
180
|
+
const toProcess = tailBuffer.subarray(0, processLength);
|
|
181
|
+
|
|
182
|
+
const decrypted = decipher.update(toProcess);
|
|
183
|
+
if (decrypted.length > 0) {
|
|
184
|
+
this.push(decrypted);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Keep only the end
|
|
188
|
+
tailBuffer = tailBuffer.subarray(processLength);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
callback();
|
|
192
|
+
} catch (err) {
|
|
193
|
+
callback(err);
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
flush(callback) {
|
|
197
|
+
try {
|
|
198
|
+
if (!headerRead) {
|
|
199
|
+
return callback(new Error('Invalid encrypted data stream: too short'));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (tailBuffer.length !== TAG_LENGTH) {
|
|
203
|
+
return callback(new Error(`Invalid encrypted data stream: missing auth tag. Got ${tailBuffer.length} bytes, expected ${TAG_LENGTH}`));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
authTag = tailBuffer;
|
|
207
|
+
decipher.setAuthTag(authTag);
|
|
208
|
+
|
|
209
|
+
const final = decipher.final();
|
|
210
|
+
if (final.length > 0) {
|
|
211
|
+
this.push(final);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
callback();
|
|
215
|
+
} catch (err) {
|
|
216
|
+
callback(new Error(`Decryption failed: wrong password or corrupt data (${err.message})`));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
93
221
|
}
|
|
94
222
|
|
|
95
223
|
/**
|