@balaji003/lantransfer 1.0.7 → 1.0.9

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/public/index.html +28 -18
  3. package/server.js +168 -57
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@balaji003/lantransfer",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "LAN File Transfer — peer-to-peer file sharing over local network. Zero dependencies.",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -347,6 +347,8 @@
347
347
  evtSource = new EventSource('/events');
348
348
  evtSource.onmessage = e => {
349
349
  state = JSON.parse(e.data);
350
+ console.log('[sse] State update — peers:', state.peers?.length, 'files:', state.sharedFiles?.length,
351
+ 'transfers:', state.transfers?.length, 'incoming:', !!state.incomingRequest);
350
352
  // Auto-select the latest file if none selected
351
353
  if (!selectedFileId && state.sharedFiles && state.sharedFiles.length > 0) {
352
354
  selectedFileId = state.sharedFiles[state.sharedFiles.length - 1].id;
@@ -354,9 +356,11 @@
354
356
  render();
355
357
  };
356
358
  evtSource.onopen = () => {
359
+ console.log('[sse] Connected');
357
360
  document.getElementById('conn-status').textContent = '';
358
361
  };
359
- evtSource.onerror = () => {
362
+ evtSource.onerror = (e) => {
363
+ console.error('[sse] Error/disconnected', e);
360
364
  document.getElementById('conn-status').textContent = 'Reconnecting...';
361
365
  };
362
366
  }
@@ -366,10 +370,16 @@
366
370
  // API
367
371
  // ──────────────────────────────────────────────────────────────────────────
368
372
  function api(endpoint, data) {
373
+ console.log('[api]', endpoint, data || '');
369
374
  return fetch('/api/' + endpoint, {
370
375
  method: 'POST',
371
376
  headers: { 'Content-Type': 'application/json' },
372
377
  body: JSON.stringify(data || {}),
378
+ }).then(r => {
379
+ console.log('[api]', endpoint, 'response:', r.status);
380
+ return r;
381
+ }).catch(err => {
382
+ console.error('[api]', endpoint, 'error:', err);
373
383
  });
374
384
  }
375
385
 
@@ -388,13 +398,9 @@
388
398
  if (selectedFileId === id) selectedFileId = null;
389
399
  }
390
400
 
391
- async function doBrowse() {
392
- if (browsing) return;
393
- browsing = true;
394
- document.getElementById('browse-btn').textContent = 'Opening...';
395
- await api('browse');
396
- browsing = false;
397
- document.getElementById('browse-btn').textContent = 'Browse';
401
+ function doBrowse() {
402
+ console.log('[browse] Clicked');
403
+ api('browse');
398
404
  }
399
405
 
400
406
  function doSend() {
@@ -403,12 +409,14 @@
403
409
  }
404
410
  }
405
411
 
406
- function respondTransfer(requestId, accepted) {
407
- api('respond', { requestId: requestId, accepted: accepted });
412
+ function respondTransfer(accepted) {
413
+ console.log('[respondTransfer] accepted:', accepted);
414
+ api('respond', { accepted: accepted });
408
415
  }
409
416
 
410
- function openFolder(filePath) {
411
- api('open-folder', { path: filePath });
417
+ function openFolder(transferId) {
418
+ console.log('[openFolder] transferId:', transferId);
419
+ api('open-folder', { transferId: transferId });
412
420
  }
413
421
 
414
422
  function doShutdown() {
@@ -531,8 +539,8 @@
531
539
  // Modal
532
540
  const overlay = document.getElementById('modal-overlay');
533
541
  const modal = document.getElementById('modal');
534
- if (state.incomingRequests && state.incomingRequests.length > 0) {
535
- const req = state.incomingRequests[0];
542
+ if (state.incomingRequest) {
543
+ const req = state.incomingRequest;
536
544
  overlay.classList.add('show');
537
545
  modal.innerHTML =
538
546
  '<div class="modal-title">Incoming File</div>'
@@ -540,8 +548,8 @@
540
548
  + '<div class="modal-file">' + esc(req.filename) + '</div>'
541
549
  + '<div class="modal-size">' + formatSize(req.size) + '</div>'
542
550
  + '<div class="modal-buttons">'
543
- + '<button class="btn-accept" onclick="respondTransfer(\'' + esc(req.id) + '\', true)">Accept</button>'
544
- + '<button class="btn-reject" onclick="respondTransfer(\'' + esc(req.id) + '\', false)">Reject</button>'
551
+ + '<button class="btn-accept" onclick="respondTransfer(true)">Accept</button>'
552
+ + '<button class="btn-reject" onclick="respondTransfer(false)">Reject</button>'
545
553
  + '</div>';
546
554
  } else {
547
555
  overlay.classList.remove('show');
@@ -573,16 +581,18 @@
573
581
  extra = '<div class="transfer-actions">'
574
582
  + '<span class="transfer-size">' + formatSize(t.size) + '</span>';
575
583
  if (t.status === 'completed' && t.direction === 'receive' && t.savePath) {
576
- extra += '<button class="btn-open-folder" onclick="openFolder(\'' + esc(t.savePath.replace(/\\/g, '\\\\')) + '\')">Open Folder</button>';
584
+ extra += '<button class="btn-open-folder" onclick="openFolder(\'' + esc(t.id) + '\')">Open Folder</button>';
577
585
  }
578
586
  extra += '</div>';
579
587
  }
580
588
 
589
+ const encrypted = (t.status === 'in_progress' || t.status === 'completed') ? ' \uD83D\uDD12' : '';
590
+
581
591
  return '<div class="transfer">'
582
592
  + '<div class="transfer-top">'
583
593
  + '<div class="transfer-info">'
584
594
  + '<span class="transfer-icon">' + icon + '</span>'
585
- + '<span>' + esc(t.filename) + ' ' + arrow + ' ' + esc(peer) + '</span>'
595
+ + '<span>' + esc(t.filename) + ' ' + arrow + ' ' + esc(peer) + encrypted + '</span>'
586
596
  + '</div>'
587
597
  + '<span class="badge ' + badgeClass + '">' + badgeText + '</span>'
588
598
  + '</div>'
package/server.js CHANGED
@@ -4,10 +4,9 @@
4
4
  * server.js — LAN File Transfer (Node.js rewrite)
5
5
  *
6
6
  * Zero external dependencies. Uses built-in modules only.
7
- * Protocol-compatible with the original Rust version:
8
- * - UDP discovery on port 34254
7
+ * - HTTP + UDP discovery
9
8
  * - TCP file transfer on port 34255
10
- * - XOR obfuscation with key "LAN-XFER-KEY-2024"
9
+ * - E2E encryption: ECDH key exchange + AES-256-CTR
11
10
  *
12
11
  * Run: node server.js
13
12
  * Then open http://localhost:3000 in a browser.
@@ -16,6 +15,7 @@
16
15
  const http = require('http');
17
16
  const fs = require('fs');
18
17
  const path = require('path');
18
+ const crypto = require('crypto');
19
19
  const dgram = require('dgram');
20
20
  const net = require('net');
21
21
  const os = require('os');
@@ -27,9 +27,9 @@ const { exec } = require('child_process');
27
27
  const DISCOVERY_PORT = 34254;
28
28
  const TRANSFER_PORT = 34255;
29
29
  const HTTP_PORT = 3000;
30
- const XOR_KEY = Buffer.from('LAN-XFER-KEY-2024');
31
30
  const PEER_TIMEOUT = 10_000; // ms
32
31
  const BROADCAST_INTERVAL = 3_000; // ms
32
+ const ECDH_CURVE = 'prime256v1'; // NIST P-256
33
33
 
34
34
  // ──────────────────────────────────────────────────────────────────────────────
35
35
  // Persistent config (device name)
@@ -62,7 +62,7 @@ let downloadDir = config.downloadDir || path.join(os.homedir(), 'Downloads');
62
62
  const sharedFiles = []; // [ { id, path, name, size } ]
63
63
  const peers = new Map(); // id → { device_name, ip, tcp_port, last_seen }
64
64
  const transfers = []; // [ TransferEntry ]
65
- const pendingRequests = new Map(); // id → { filename, size, senderName, resolve }
65
+ let pendingRequest = null; // { filename, size, senderName, resolve } or null
66
66
  let idCounter = 0;
67
67
  let browseInProgress = false;
68
68
 
@@ -114,9 +114,9 @@ function serializeState() {
114
114
  speed: t.speed, error: t.error,
115
115
  savePath: t.savePath || null,
116
116
  })),
117
- incomingRequests: [...pendingRequests.entries()].map(([id, r]) => ({
118
- id, filename: r.filename, size: r.size, senderName: r.senderName,
119
- })),
117
+ incomingRequest: pendingRequest
118
+ ? { filename: pendingRequest.filename, size: pendingRequest.size, senderName: pendingRequest.senderName }
119
+ : null,
120
120
  });
121
121
  }
122
122
 
@@ -128,14 +128,18 @@ function broadcast() {
128
128
  }
129
129
 
130
130
  // ──────────────────────────────────────────────────────────────────────────────
131
- // Helpers
131
+ // Helpers — E2E encryption (ECDH + AES-256-CTR)
132
132
  // ──────────────────────────────────────────────────────────────────────────────
133
- function xorCrypt(buf, offset) {
134
- const out = Buffer.allocUnsafe(buf.length);
135
- for (let i = 0; i < buf.length; i++) {
136
- out[i] = buf[i] ^ XOR_KEY[(offset + i) % XOR_KEY.length];
137
- }
138
- return out;
133
+ function createKeyPair() {
134
+ const ecdh = crypto.createECDH(ECDH_CURVE);
135
+ ecdh.generateKeys();
136
+ return ecdh;
137
+ }
138
+
139
+ function deriveKey(ecdh, peerPubKey) {
140
+ const shared = ecdh.computeSecret(peerPubKey);
141
+ // SHA-256 the shared secret to get a 32-byte AES key
142
+ return crypto.createHash('sha256').update(shared).digest();
139
143
  }
140
144
 
141
145
  function readBody(req) {
@@ -180,6 +184,8 @@ function readExact(socket, n) {
180
184
  const htmlPath = path.join(__dirname, 'public', 'index.html');
181
185
 
182
186
  const server = http.createServer(async (req, res) => {
187
+ console.log(` [http] ${req.method} ${req.url}`);
188
+
183
189
  // ── Serve the web UI ──
184
190
  if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
185
191
  fs.readFile(htmlPath, (err, data) => {
@@ -217,7 +223,11 @@ const server = http.createServer(async (req, res) => {
217
223
  'Connection': 'keep-alive',
218
224
  });
219
225
  sseClients.add(res);
220
- req.on('close', () => sseClients.delete(res));
226
+ console.log(` [sse] Client connected (total: ${sseClients.size})`);
227
+ req.on('close', () => {
228
+ sseClients.delete(res);
229
+ console.log(` [sse] Client disconnected (total: ${sseClients.size})`);
230
+ });
221
231
  res.write(`data: ${serializeState()}\n\n`);
222
232
  return;
223
233
  }
@@ -233,32 +243,40 @@ const server = http.createServer(async (req, res) => {
233
243
  console.log(` [toggle] Discoverable: ${isDiscoverable}`);
234
244
  }
235
245
  else if (req.url === '/api/browse') {
246
+ // Respond immediately so we don't block the HTTP connection
247
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
248
+ res.end('ok');
236
249
  if (browseInProgress) {
237
250
  console.log(' [browse] Dialog already open, ignoring');
238
251
  } else {
239
252
  browseInProgress = true;
240
253
  console.log(' [browse] Opening file dialog...');
241
- const fp = await openFileDialog();
242
- browseInProgress = false;
243
- if (fp) {
244
- try {
254
+ try {
255
+ const fp = await openFileDialog();
256
+ browseInProgress = false;
257
+ if (fp) {
245
258
  const s = fs.statSync(fp);
246
259
  const file = { id: String(idCounter++), path: fp, name: path.basename(fp), size: s.size };
247
260
  sharedFiles.push(file);
248
261
  console.log(` [browse] Added: ${file.name} (${formatSize(file.size)})`);
249
- } catch (e) {
250
- console.error(` [browse] Error reading file: ${e.message}`);
262
+ } else {
263
+ console.log(' [browse] Cancelled by user');
251
264
  }
252
- } else {
253
- console.log(' [browse] Cancelled');
265
+ } catch (e) {
266
+ browseInProgress = false;
267
+ console.error(` [browse] Error: ${e.message}`);
254
268
  }
269
+ broadcast();
255
270
  }
271
+ return; // already responded
256
272
  }
257
273
  else if (req.url === '/api/remove-file') {
258
274
  const idx = sharedFiles.findIndex(f => f.id === data.fileId);
259
275
  if (idx !== -1) {
260
276
  console.log(` [files] Removed: ${sharedFiles[idx].name}`);
261
277
  sharedFiles.splice(idx, 1);
278
+ } else {
279
+ console.log(` [files] Remove failed — fileId not found: ${data.fileId}`);
262
280
  }
263
281
  }
264
282
  else if (req.url === '/api/send') {
@@ -266,9 +284,9 @@ const server = http.createServer(async (req, res) => {
266
284
  const file = sharedFiles.find(f => f.id === data.fileId);
267
285
  if (peer && file) {
268
286
  console.log(` [send] ${file.name} -> ${peer.device_name} (${peer.ip})`);
269
- sendFile(peer, { ...file });
287
+ sendFile(peer, { ...file }).catch(e => console.error(` [send] Unhandled: ${e.message}`));
270
288
  } else {
271
- console.log(` [send] Failed — peer: ${!!peer}, file: ${!!file}`);
289
+ console.log(` [send] Failed — peer: ${!!peer} (${data.peerId}), file: ${!!file} (${data.fileId})`);
272
290
  }
273
291
  }
274
292
  else if (req.url === '/api/rename') {
@@ -278,34 +296,62 @@ const server = http.createServer(async (req, res) => {
278
296
  config.deviceName = newName;
279
297
  saveConfig(config);
280
298
  console.log(` [rename] Device name set to: ${deviceName}`);
299
+ } else {
300
+ console.log(` [rename] Invalid name: "${data.name}"`);
281
301
  }
282
302
  }
283
303
  else if (req.url === '/api/set-download-dir') {
284
- // Open native folder picker
285
- const dir = await openFolderDialog();
286
- if (dir) {
287
- downloadDir = dir;
288
- config.downloadDir = dir;
289
- saveConfig(config);
290
- console.log(` [config] Download dir set to: ${downloadDir}`);
304
+ // Respond immediately so we don't block the HTTP connection
305
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
306
+ res.end('ok');
307
+ console.log(' [config] Opening folder dialog...');
308
+ try {
309
+ const dir = await openFolderDialog();
310
+ if (dir) {
311
+ downloadDir = dir;
312
+ config.downloadDir = dir;
313
+ saveConfig(config);
314
+ console.log(` [config] Download dir set to: ${downloadDir}`);
315
+ } else {
316
+ console.log(' [config] Folder dialog cancelled');
317
+ }
318
+ } catch (e) {
319
+ console.error(` [config] Folder dialog error: ${e.message}`);
291
320
  }
321
+ broadcast();
322
+ return; // already responded
292
323
  }
293
324
  else if (req.url === '/api/respond') {
294
- const pending = pendingRequests.get(data.requestId);
295
- if (pending) {
296
- pending.resolve(!!data.accepted);
297
- pendingRequests.delete(data.requestId);
298
- console.log(` [respond] ${data.accepted ? 'Accepted' : 'Rejected'} transfer ${data.requestId}`);
325
+ console.log(` [respond] Received: accepted=${data.accepted}`);
326
+ if (pendingRequest) {
327
+ pendingRequest.resolve(!!data.accepted);
328
+ console.log(` [respond] ${data.accepted ? 'Accepted' : 'Rejected'}: ${pendingRequest.filename}`);
329
+ pendingRequest = null;
330
+ } else {
331
+ console.log(` [respond] WARNING: No pending request`);
299
332
  }
300
333
  }
301
334
  else if (req.url === '/api/open-folder') {
302
- const filePath = data.path;
303
- if (filePath && fs.existsSync(filePath)) {
304
- const dir = path.dirname(filePath);
305
- if (process.platform === 'win32') exec(`explorer /select,"${filePath}"`);
306
- else if (process.platform === 'darwin') exec(`open -R "${filePath}"`);
307
- else exec(`xdg-open "${dir}"`);
308
- console.log(` [open] ${filePath}`);
335
+ const transferId = data.transferId;
336
+ const t = transfers.find(tr => tr.id === transferId);
337
+ if (t && t.savePath) {
338
+ console.log(` [open] Opening folder for: ${t.savePath}`);
339
+ if (fs.existsSync(t.savePath)) {
340
+ if (process.platform === 'win32') exec(`explorer /select,"${t.savePath}"`);
341
+ else if (process.platform === 'darwin') exec(`open -R "${t.savePath}"`);
342
+ else exec(`xdg-open "${path.dirname(t.savePath)}"`);
343
+ } else {
344
+ console.log(` [open] File no longer exists: ${t.savePath}`);
345
+ // Open the directory instead
346
+ const dir = path.dirname(t.savePath);
347
+ if (fs.existsSync(dir)) {
348
+ if (process.platform === 'win32') exec(`explorer "${dir}"`);
349
+ else if (process.platform === 'darwin') exec(`open "${dir}"`);
350
+ else exec(`xdg-open "${dir}"`);
351
+ }
352
+ }
353
+ } else {
354
+ console.log(` [open] Failed — transferId: ${transferId}, found: ${!!t}, savePath: ${t ? t.savePath : 'N/A'}`);
309
355
  }
310
356
  }
311
357
  else if (req.url === '/api/shutdown') {
@@ -315,6 +361,9 @@ const server = http.createServer(async (req, res) => {
315
361
  setTimeout(() => process.exit(0), 200);
316
362
  return;
317
363
  }
364
+ else {
365
+ console.log(` [http] Unknown POST endpoint: ${req.url}`);
366
+ }
318
367
 
319
368
  res.writeHead(200, { 'Content-Type': 'text/plain' });
320
369
  res.end('ok');
@@ -428,33 +477,41 @@ tcpServer.on('error', err => {
428
477
  tcpServer.listen(TRANSFER_PORT);
429
478
 
430
479
  async function handleIncoming(socket) {
480
+ const remoteAddr = `${socket.remoteAddress}:${socket.remotePort}`;
481
+ console.log(` [recv] TCP connection from ${remoteAddr}`);
482
+
431
483
  // Read header length (8 bytes, little-endian u64)
432
484
  const lenBuf = await readExact(socket, 8);
433
485
  const headerLen = Number(lenBuf.readBigUInt64LE(0));
486
+ console.log(` [recv] Header length: ${headerLen} bytes`);
434
487
 
435
488
  // Read header JSON
436
489
  const headerBuf = await readExact(socket, headerLen);
437
490
  const header = JSON.parse(headerBuf.toString('utf-8'));
438
491
  const { filename, size, sender_name } = header;
439
492
 
440
- console.log(` Incoming: ${filename} (${formatSize(size)}) from ${sender_name}`);
493
+ console.log(` [recv] Incoming: "${filename}" (${formatSize(size)}) from ${sender_name}`);
441
494
 
442
495
  // Prompt user for accept / reject
443
- const reqId = String(idCounter++);
496
+ console.log(` [recv] Waiting for user decision...`);
497
+
444
498
  const accepted = await new Promise(resolve => {
445
- pendingRequests.set(reqId, { filename, size, senderName: sender_name, resolve });
499
+ pendingRequest = { filename, size, senderName: sender_name, resolve };
446
500
  broadcast();
447
501
 
448
502
  // Auto-reject if sender disconnects while waiting
449
503
  socket.once('close', () => {
450
- if (pendingRequests.has(reqId)) {
451
- pendingRequests.delete(reqId);
504
+ if (pendingRequest && pendingRequest.resolve === resolve) {
505
+ console.log(` [recv] Sender disconnected while waiting, auto-rejecting`);
506
+ pendingRequest = null;
452
507
  resolve(false);
453
508
  broadcast();
454
509
  }
455
510
  });
456
511
  });
457
512
 
513
+ console.log(` [recv] User decision: ${accepted ? 'ACCEPTED' : 'REJECTED'}`);
514
+
458
515
  // Send decision byte
459
516
  socket.write(Buffer.from([accepted ? 1 : 0]));
460
517
  if (!accepted) { socket.end(); return; }
@@ -481,13 +538,34 @@ async function handleIncoming(socket) {
481
538
  }
482
539
 
483
540
  t.savePath = savePath;
484
-
541
+ console.log(` [recv] Saving to: ${savePath}`);
542
+
543
+ // ── E2E key exchange: receive sender's public key, send ours ──
544
+ const pubKeyLenBuf = await readExact(socket, 2);
545
+ const pubKeyLen = pubKeyLenBuf.readUInt16BE(0);
546
+ const senderPubKey = await readExact(socket, pubKeyLen);
547
+ console.log(` [recv] Got sender public key (${pubKeyLen} bytes)`);
548
+
549
+ const ecdh = createKeyPair();
550
+ const myPubKey = ecdh.getPublicKey();
551
+ const keyLenBuf = Buffer.alloc(2);
552
+ keyLenBuf.writeUInt16BE(myPubKey.length);
553
+ socket.write(keyLenBuf);
554
+ socket.write(myPubKey);
555
+ console.log(` [recv] Sent our public key (${myPubKey.length} bytes)`);
556
+
557
+ // Derive AES key + receive IV
558
+ const aesKey = deriveKey(ecdh, senderPubKey);
559
+ const iv = await readExact(socket, 16);
560
+ console.log(` [recv] AES key derived, IV received — decrypting with AES-256-CTR`);
561
+
562
+ const decipher = crypto.createDecipheriv('aes-256-ctr', aesKey, iv);
485
563
  const ws = fs.createWriteStream(savePath);
486
564
  let received = 0;
487
565
  let lastBc = 0;
488
566
 
489
567
  socket.on('data', chunk => {
490
- const decrypted = xorCrypt(chunk, received);
568
+ const decrypted = decipher.update(chunk);
491
569
  ws.write(decrypted);
492
570
  received += chunk.length;
493
571
 
@@ -504,11 +582,13 @@ async function handleIncoming(socket) {
504
582
 
505
583
  await new Promise((resolve, reject) => {
506
584
  socket.on('end', () => {
585
+ const final = decipher.final();
586
+ if (final.length > 0) ws.write(final);
507
587
  ws.end();
508
588
  t.status = 'completed';
509
589
  t.progress = 100;
510
590
  broadcast();
511
- console.log(` Saved: ${savePath}`);
591
+ console.log(` [recv] Complete: ${savePath} (${formatSize(received)})`);
512
592
  resolve();
513
593
  });
514
594
  socket.on('error', err => {
@@ -516,6 +596,7 @@ async function handleIncoming(socket) {
516
596
  t.status = 'failed';
517
597
  t.error = err.message;
518
598
  broadcast();
599
+ console.error(` [recv] Socket error: ${err.message}`);
519
600
  reject(err);
520
601
  });
521
602
  });
@@ -540,6 +621,7 @@ async function sendFile(peer, file) {
540
621
  socket.connect(peer.tcp_port, peer.ip, res);
541
622
  socket.once('error', rej);
542
623
  });
624
+ console.log(` [send] Connected to ${peer.ip}:${peer.tcp_port}`);
543
625
 
544
626
  // Send header
545
627
  const headerJSON = JSON.stringify({
@@ -558,20 +640,44 @@ async function sendFile(peer, file) {
558
640
  t.error = 'Rejected by recipient';
559
641
  broadcast();
560
642
  socket.end();
643
+ console.log(` [send] Rejected by ${peer.device_name}`);
561
644
  return;
562
645
  }
563
646
 
647
+ console.log(` [send] Accepted — starting E2E key exchange`);
648
+
649
+ // ── E2E key exchange: send our public key, receive receiver's ──
650
+ const ecdh = createKeyPair();
651
+ const myPubKey = ecdh.getPublicKey();
652
+ const keyLenBuf = Buffer.alloc(2);
653
+ keyLenBuf.writeUInt16BE(myPubKey.length);
654
+ socket.write(keyLenBuf);
655
+ socket.write(myPubKey);
656
+ console.log(` [send] Sent our public key (${myPubKey.length} bytes)`);
657
+
658
+ const recvKeyLenBuf = await readExact(socket, 2);
659
+ const recvKeyLen = recvKeyLenBuf.readUInt16BE(0);
660
+ const recvPubKey = await readExact(socket, recvKeyLen);
661
+ console.log(` [send] Got receiver public key (${recvKeyLen} bytes)`);
662
+
663
+ // Derive AES key, generate IV, send IV
664
+ const aesKey = deriveKey(ecdh, recvPubKey);
665
+ const iv = crypto.randomBytes(16);
666
+ socket.write(iv);
667
+ console.log(` [send] AES key derived, IV sent — encrypting with AES-256-CTR`);
668
+
564
669
  t.status = 'in_progress';
565
670
  t._start = Date.now();
566
671
  broadcast();
567
672
 
568
- // Stream file with XOR encryption
673
+ // Stream file with AES-256-CTR encryption
674
+ const cipher = crypto.createCipheriv('aes-256-ctr', aesKey, iv);
569
675
  const rs = fs.createReadStream(file.path, { highWaterMark: 65536 });
570
676
  let sent = 0;
571
677
  let lastBc = 0;
572
678
 
573
679
  for await (const chunk of rs) {
574
- const encrypted = xorCrypt(Buffer.from(chunk), sent);
680
+ const encrypted = cipher.update(Buffer.from(chunk));
575
681
  const ok = socket.write(encrypted);
576
682
  sent += chunk.length;
577
683
 
@@ -594,16 +700,20 @@ async function sendFile(peer, file) {
594
700
  }
595
701
  }
596
702
 
703
+ // Write final cipher block
704
+ const final = cipher.final();
705
+ if (final.length > 0) socket.write(final);
706
+
597
707
  socket.end();
598
708
  t.status = 'completed';
599
709
  t.progress = 100;
600
710
  broadcast();
601
- console.log(` Sent: ${file.name} → ${peer.device_name}`);
711
+ console.log(` [send] Complete: ${file.name} → ${peer.device_name}`);
602
712
  } catch (e) {
603
713
  t.status = 'failed';
604
714
  t.error = e.message;
605
715
  broadcast();
606
- console.error(` Send error: ${e.message}`);
716
+ console.error(` [send] Error: ${e.message}`);
607
717
  }
608
718
  }
609
719
 
@@ -715,6 +825,7 @@ server.listen(HTTP_PORT, () => {
715
825
  console.log(` Discovery HTTP scan (no firewall needed)`);
716
826
  console.log(` Discovery UDP :${DISCOVERY_PORT} (fallback)`);
717
827
  console.log(` Transfer TCP :${TRANSFER_PORT}`);
828
+ console.log(` Encryption ECDH + AES-256-CTR (E2E)`);
718
829
  console.log(` UI: http://localhost:${HTTP_PORT}`);
719
830
  console.log('');
720
831