nodio-cli 1.1.3 → 1.1.4
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/package.json +1 -1
- package/src/server/index.js +4 -1
- package/src/server/routes.js +61 -0
- package/src/user/commands.js +65 -29
package/package.json
CHANGED
package/src/server/index.js
CHANGED
|
@@ -19,7 +19,10 @@ async function startServer() {
|
|
|
19
19
|
const corsAllowlist = new Set([
|
|
20
20
|
'https://nodio.me',
|
|
21
21
|
'https://drive.nodio.me',
|
|
22
|
-
'https://effective-space-rotary-phone-wrv6xg64p7w72wj-3000.app.github.dev'
|
|
22
|
+
'https://effective-space-rotary-phone-wrv6xg64p7w72wj-3000.app.github.dev',
|
|
23
|
+
'https://cautious-sniffle-wrv6xg64pg7jhq7x-5173.app.github.dev',
|
|
24
|
+
'http://localhost:5173',
|
|
25
|
+
'http://127.0.0.1:5173'
|
|
23
26
|
]);
|
|
24
27
|
|
|
25
28
|
const corsOptions = {
|
package/src/server/routes.js
CHANGED
|
@@ -572,6 +572,67 @@ function buildRoutes(config) {
|
|
|
572
572
|
}
|
|
573
573
|
});
|
|
574
574
|
|
|
575
|
+
router.post('/files/:fileId/store-key', verifyToken, async (req, res, next) => {
|
|
576
|
+
try {
|
|
577
|
+
const { fileId } = req.params;
|
|
578
|
+
const encryptedAESKey = String(req.body?.encryptedAESKey || '').trim();
|
|
579
|
+
|
|
580
|
+
if (!fileId) {
|
|
581
|
+
return res.status(400).json({ error: 'fileId is required' });
|
|
582
|
+
}
|
|
583
|
+
if (!encryptedAESKey) {
|
|
584
|
+
return res.status(400).json({ error: 'encryptedAESKey is required' });
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const file = await FileModel.findOne({ fileId });
|
|
588
|
+
if (!file) {
|
|
589
|
+
return res.status(404).json({ error: 'file not found' });
|
|
590
|
+
}
|
|
591
|
+
if (file.userId !== req.userId) {
|
|
592
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
await FileModel.findOneAndUpdate(
|
|
596
|
+
{ fileId },
|
|
597
|
+
{
|
|
598
|
+
$set: {
|
|
599
|
+
encryptedAESKey
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
{ new: true }
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
res.json({ ok: true });
|
|
606
|
+
} catch (error) {
|
|
607
|
+
next(error);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
router.get('/files/:fileId/key', verifyToken, async (req, res, next) => {
|
|
612
|
+
try {
|
|
613
|
+
const { fileId } = req.params;
|
|
614
|
+
|
|
615
|
+
if (!fileId) {
|
|
616
|
+
return res.status(400).json({ error: 'fileId is required' });
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const file = await FileModel.findOne({ fileId }).lean();
|
|
620
|
+
if (!file) {
|
|
621
|
+
return res.status(404).json({ error: 'file not found' });
|
|
622
|
+
}
|
|
623
|
+
if (file.userId !== req.userId) {
|
|
624
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
625
|
+
}
|
|
626
|
+
if (!file.encryptedAESKey) {
|
|
627
|
+
return res.status(404).json({ error: 'encrypted key not found' });
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
res.json({ encryptedAESKey: file.encryptedAESKey });
|
|
631
|
+
} catch (error) {
|
|
632
|
+
next(error);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
575
636
|
router.post('/files/:fileId/filecoin', verifyToken, async (req, res, next) => {
|
|
576
637
|
try {
|
|
577
638
|
const { fileId } = req.params;
|
package/src/user/commands.js
CHANGED
|
@@ -13,7 +13,6 @@ const {
|
|
|
13
13
|
clearSession,
|
|
14
14
|
deriveMasterKey,
|
|
15
15
|
encryptMasterKey,
|
|
16
|
-
decryptMasterKey
|
|
17
16
|
} = require('../cli/session');
|
|
18
17
|
|
|
19
18
|
function promptInput(promptText) {
|
|
@@ -32,9 +31,7 @@ function promptHiddenInput(promptText) {
|
|
|
32
31
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
33
32
|
rl.stdoutMuted = true;
|
|
34
33
|
rl._writeToOutput = function _writeToOutput(stringToWrite) {
|
|
35
|
-
if (rl.stdoutMuted) {
|
|
36
|
-
rl.output.write('*');
|
|
37
|
-
} else {
|
|
34
|
+
if (!rl.stdoutMuted) {
|
|
38
35
|
rl.output.write(stringToWrite);
|
|
39
36
|
}
|
|
40
37
|
};
|
|
@@ -50,7 +47,7 @@ function promptHiddenInput(promptText) {
|
|
|
50
47
|
async function requireSession() {
|
|
51
48
|
const session = await loadSession();
|
|
52
49
|
if (!session) {
|
|
53
|
-
console.log('Please login
|
|
50
|
+
console.log('Please login: nodio login');
|
|
54
51
|
process.exit(1);
|
|
55
52
|
}
|
|
56
53
|
return session;
|
|
@@ -58,7 +55,8 @@ async function requireSession() {
|
|
|
58
55
|
|
|
59
56
|
function attachSessionToken(api, session) {
|
|
60
57
|
if (session?.apiToken) {
|
|
61
|
-
api.defaults.headers
|
|
58
|
+
api.defaults.headers.common.Authorization = `Bearer ${session.apiToken}`;
|
|
59
|
+
api.defaults.headers.common['x-api-token'] = session.apiToken;
|
|
62
60
|
}
|
|
63
61
|
}
|
|
64
62
|
|
|
@@ -92,8 +90,8 @@ async function buildSessionPayload(authResponse, password) {
|
|
|
92
90
|
|
|
93
91
|
async function login(options) {
|
|
94
92
|
const serverUrl = options.server;
|
|
95
|
-
const email = await promptInput('
|
|
96
|
-
const password = await promptHiddenInput('
|
|
93
|
+
const email = await promptInput('Enter your email: ');
|
|
94
|
+
const password = await promptHiddenInput('Enter your account password: ');
|
|
97
95
|
|
|
98
96
|
const api = createApiClient(serverUrl);
|
|
99
97
|
const response = await api.post('/auth/login', { email, password });
|
|
@@ -104,8 +102,8 @@ async function login(options) {
|
|
|
104
102
|
|
|
105
103
|
async function register(options) {
|
|
106
104
|
const serverUrl = options.server;
|
|
107
|
-
const email = await promptInput('
|
|
108
|
-
const password = await promptHiddenInput('
|
|
105
|
+
const email = await promptInput('Enter your email: ');
|
|
106
|
+
const password = await promptHiddenInput('Enter your account password: ');
|
|
109
107
|
|
|
110
108
|
const api = createApiClient(serverUrl);
|
|
111
109
|
const response = await api.post('/auth/register', { email, password });
|
|
@@ -408,7 +406,6 @@ async function uploadFile(options) {
|
|
|
408
406
|
const relayFirst = Boolean(options.relayFirst);
|
|
409
407
|
|
|
410
408
|
const session = await requireSession();
|
|
411
|
-
const masterKey = decryptMasterKey(session.encryptedMasterKey, session.apiToken);
|
|
412
409
|
|
|
413
410
|
if (!Number.isFinite(shardSizeMb) || shardSizeMb <= 0) {
|
|
414
411
|
throw new Error('shard-size-mb must be greater than 0');
|
|
@@ -426,8 +423,6 @@ async function uploadFile(options) {
|
|
|
426
423
|
|
|
427
424
|
const keyBuffer = options.keyBase64 ? parseAesKey(options.keyBase64) : crypto.randomBytes(32);
|
|
428
425
|
const keyBase64 = keyBuffer.toString('base64');
|
|
429
|
-
const encryptedKeyPayload = encryptAes256Gcm(keyBuffer, masterKey);
|
|
430
|
-
const encryptedAESKey = packEncryptedKey(encryptedKeyPayload);
|
|
431
426
|
|
|
432
427
|
const api = createApiClient(serverUrl);
|
|
433
428
|
attachSessionToken(api, session);
|
|
@@ -440,7 +435,6 @@ async function uploadFile(options) {
|
|
|
440
435
|
sizeBytes: plainBuffer.length,
|
|
441
436
|
shardCount: chunks.length,
|
|
442
437
|
cipher: 'aes-256-gcm',
|
|
443
|
-
encryptedAESKey,
|
|
444
438
|
metadata: {
|
|
445
439
|
encryption: {
|
|
446
440
|
algorithm: 'aes-256-gcm',
|
|
@@ -568,7 +562,6 @@ async function uploadFile(options) {
|
|
|
568
562
|
sizeBytes: plainBuffer.length,
|
|
569
563
|
shardCount: chunks.length,
|
|
570
564
|
cipher: 'aes-256-gcm',
|
|
571
|
-
encryptedAESKey,
|
|
572
565
|
metadata: {
|
|
573
566
|
encryption: {
|
|
574
567
|
algorithm: 'aes-256-gcm',
|
|
@@ -584,13 +577,35 @@ async function uploadFile(options) {
|
|
|
584
577
|
console.log(`[filecoin] uploading to Filecoin via central server (this may take a while)...`);
|
|
585
578
|
await requestFilecoinBackup(api, encryptedFileBuffer, fileId);
|
|
586
579
|
|
|
587
|
-
console.log(`Upload complete`);
|
|
580
|
+
console.log(`Upload complete: fileId ${fileId}`);
|
|
588
581
|
console.log(`fileId: ${fileId}`);
|
|
589
582
|
console.log(`originalName: ${originalName}`);
|
|
590
583
|
console.log(`sizeBytes: ${plainBuffer.length}`);
|
|
591
584
|
console.log(`shardCount: ${chunks.length}`);
|
|
592
585
|
console.log(`replicasPerShard: ${replicas}`);
|
|
593
|
-
|
|
586
|
+
|
|
587
|
+
if (session?.argon2Salt && session?.apiToken) {
|
|
588
|
+
console.log('Master password required to save the file key.');
|
|
589
|
+
const masterPassword = await promptHiddenInput('Enter your master password now: ');
|
|
590
|
+
try {
|
|
591
|
+
const masterKey = await deriveMasterKey(masterPassword, session.argon2Salt);
|
|
592
|
+
const encryptedKeyPayload = encryptAes256Gcm(keyBuffer, masterKey);
|
|
593
|
+
const encryptedAESKey = packEncryptedKey(encryptedKeyPayload);
|
|
594
|
+
|
|
595
|
+
await api.post(`/files/${fileId}/store-key`, {
|
|
596
|
+
encryptedAESKey
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
console.log('Key saved securely ✅');
|
|
600
|
+
} catch (error) {
|
|
601
|
+
if (String(error?.message || '').includes('unsupported state or unable to authenticate data')) {
|
|
602
|
+
throw new Error('Wrong master password');
|
|
603
|
+
}
|
|
604
|
+
throw error;
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
console.log(`aes256KeyBase64: ${keyBase64}`);
|
|
608
|
+
}
|
|
594
609
|
}
|
|
595
610
|
|
|
596
611
|
async function downloadFile(options) {
|
|
@@ -601,7 +616,6 @@ async function downloadFile(options) {
|
|
|
601
616
|
const relayFirst = Boolean(options.relayFirst);
|
|
602
617
|
|
|
603
618
|
const session = await requireSession();
|
|
604
|
-
const masterKey = decryptMasterKey(session.encryptedMasterKey, session.apiToken);
|
|
605
619
|
|
|
606
620
|
if (!Number.isFinite(directTimeoutMs) || directTimeoutMs <= 0) {
|
|
607
621
|
throw new Error('direct-timeout-ms must be greater than 0');
|
|
@@ -617,16 +631,36 @@ async function downloadFile(options) {
|
|
|
617
631
|
let keyBuffer = null;
|
|
618
632
|
if (options.keyBase64) {
|
|
619
633
|
keyBuffer = parseAesKey(options.keyBase64);
|
|
620
|
-
} else
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
634
|
+
} else {
|
|
635
|
+
console.log('Fetching key from account...');
|
|
636
|
+
console.log('Master password required to decrypt the file key.');
|
|
637
|
+
const masterPassword = await promptHiddenInput('Enter your master password now: ');
|
|
638
|
+
|
|
639
|
+
try {
|
|
640
|
+
const masterKey = await deriveMasterKey(masterPassword, session.argon2Salt);
|
|
641
|
+
const keyResponse = await api.get(`/files/${fileId}/key`);
|
|
642
|
+
const encryptedAESKey = keyResponse.data?.encryptedAESKey;
|
|
643
|
+
|
|
644
|
+
if (!encryptedAESKey) {
|
|
645
|
+
throw new Error('missing stored key');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const encryptedKeyPayload = unpackEncryptedKey(encryptedAESKey);
|
|
649
|
+
keyBuffer = decryptAes256Gcm(
|
|
650
|
+
encryptedKeyPayload.cipherText,
|
|
651
|
+
masterKey,
|
|
652
|
+
encryptedKeyPayload.iv,
|
|
653
|
+
encryptedKeyPayload.authTag
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
if (!Buffer.isBuffer(keyBuffer) || keyBuffer.length !== 32) {
|
|
657
|
+
throw new Error('invalid decrypted AES key length');
|
|
658
|
+
}
|
|
659
|
+
} catch (error) {
|
|
660
|
+
if (String(error?.message || '').includes('unsupported state or unable to authenticate data')) {
|
|
661
|
+
throw new Error('Wrong master password');
|
|
662
|
+
}
|
|
663
|
+
throw error;
|
|
630
664
|
}
|
|
631
665
|
}
|
|
632
666
|
|
|
@@ -638,6 +672,8 @@ async function downloadFile(options) {
|
|
|
638
672
|
throw new Error('missing or unsupported encryption metadata');
|
|
639
673
|
}
|
|
640
674
|
|
|
675
|
+
console.log('Downloading and decrypting...');
|
|
676
|
+
|
|
641
677
|
const shardMetaMap = new Map((encryption.shards || []).map((entry) => [entry.shardId, entry]));
|
|
642
678
|
|
|
643
679
|
const orderedShards = [...(manifest.shards || [])].sort((a, b) => a.order - b.order);
|
|
@@ -732,7 +768,7 @@ async function downloadFile(options) {
|
|
|
732
768
|
await fs.mkdir(path.dirname(output), { recursive: true });
|
|
733
769
|
await fs.writeFile(output, reconstructed);
|
|
734
770
|
|
|
735
|
-
console.log(
|
|
771
|
+
console.log('Done ✅');
|
|
736
772
|
console.log(`fileId: ${fileId}`);
|
|
737
773
|
console.log(`output: ${output}`);
|
|
738
774
|
console.log(`sizeBytes: ${reconstructed.length}`);
|