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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodio-cli",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Nodio distributed storage network",
5
5
  "main": "src/server/index.js",
6
6
  "type": "commonjs",
@@ -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 = {
@@ -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;
@@ -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 first using: npx nodio-cli nodio 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['x-api-token'] = session.apiToken;
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('Email: ');
96
- const password = await promptHiddenInput('Password: ');
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('Email: ');
108
- const password = await promptHiddenInput('Password: ');
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
- console.log(`aes256KeyBase64: ${keyBase64}`);
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 if (manifest.file?.encryptedAESKey) {
621
- const encryptedKeyPayload = unpackEncryptedKey(manifest.file.encryptedAESKey);
622
- keyBuffer = decryptAes256Gcm(
623
- encryptedKeyPayload.cipherText,
624
- masterKey,
625
- encryptedKeyPayload.iv,
626
- encryptedKeyPayload.authTag
627
- );
628
- if (!Buffer.isBuffer(keyBuffer) || keyBuffer.length !== 32) {
629
- throw new Error('invalid decrypted AES key length');
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(`Download complete`);
771
+ console.log('Done ✅');
736
772
  console.log(`fileId: ${fileId}`);
737
773
  console.log(`output: ${output}`);
738
774
  console.log(`sizeBytes: ${reconstructed.length}`);