nodebb-plugin-pdf-secure 1.2.0 → 1.2.1
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/lib/controllers.js +28 -1
- package/package.json +1 -1
- package/static/viewer.html +45 -4
package/lib/controllers.js
CHANGED
|
@@ -5,6 +5,28 @@ const pdfHandler = require('./pdf-handler');
|
|
|
5
5
|
|
|
6
6
|
const Controllers = module.exports;
|
|
7
7
|
|
|
8
|
+
// XOR key for partial encryption (rotates through this pattern)
|
|
9
|
+
const XOR_KEY = [0x5A, 0x3C, 0x7E, 0x1F, 0x9D, 0xB2, 0x4A, 0xE8];
|
|
10
|
+
|
|
11
|
+
// Partial XOR - encrypts first 10KB and every 50th byte after that
|
|
12
|
+
function partialXorEncode(buffer) {
|
|
13
|
+
const data = Buffer.from(buffer);
|
|
14
|
+
const keyLen = XOR_KEY.length;
|
|
15
|
+
|
|
16
|
+
// Encrypt first 10KB fully
|
|
17
|
+
const fullEncryptLen = Math.min(10240, data.length);
|
|
18
|
+
for (let i = 0; i < fullEncryptLen; i++) {
|
|
19
|
+
data[i] = data[i] ^ XOR_KEY[i % keyLen];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Encrypt every 50th byte after that
|
|
23
|
+
for (let i = fullEncryptLen; i < data.length; i += 50) {
|
|
24
|
+
data[i] = data[i] ^ XOR_KEY[i % keyLen];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return data;
|
|
28
|
+
}
|
|
29
|
+
|
|
8
30
|
Controllers.renderAdminPage = function (req, res) {
|
|
9
31
|
res.render('admin/plugins/pdf-secure', {
|
|
10
32
|
title: 'PDF Secure Viewer',
|
|
@@ -34,14 +56,19 @@ Controllers.servePdfBinary = async function (req, res) {
|
|
|
34
56
|
pdfBuffer = await pdfHandler.getSinglePagePdf(data.file);
|
|
35
57
|
}
|
|
36
58
|
|
|
59
|
+
// Apply partial XOR encryption
|
|
60
|
+
const encodedBuffer = partialXorEncode(pdfBuffer);
|
|
61
|
+
|
|
37
62
|
res.set({
|
|
38
63
|
'Content-Type': 'application/octet-stream',
|
|
39
64
|
'Cache-Control': 'no-store, no-cache, must-revalidate, private',
|
|
40
65
|
'X-Content-Type-Options': 'nosniff',
|
|
41
66
|
'Content-Disposition': 'inline',
|
|
67
|
+
// Send XOR key as header for client decoding
|
|
68
|
+
'X-PDF-Key': Buffer.from(XOR_KEY).toString('base64'),
|
|
42
69
|
});
|
|
43
70
|
|
|
44
|
-
return res.send(
|
|
71
|
+
return res.send(encodedBuffer);
|
|
45
72
|
} catch (err) {
|
|
46
73
|
if (err.message === 'File not found') {
|
|
47
74
|
return res.status(404).json({ error: 'PDF not found' });
|
package/package.json
CHANGED
package/static/viewer.html
CHANGED
|
@@ -1420,6 +1420,26 @@
|
|
|
1420
1420
|
generateThumbnails();
|
|
1421
1421
|
}
|
|
1422
1422
|
|
|
1423
|
+
// Partial XOR decoder - must match backend encoding
|
|
1424
|
+
function partialXorDecode(encodedData, keyBase64) {
|
|
1425
|
+
const key = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
|
|
1426
|
+
const data = new Uint8Array(encodedData);
|
|
1427
|
+
const keyLen = key.length;
|
|
1428
|
+
|
|
1429
|
+
// Decrypt first 10KB fully
|
|
1430
|
+
const fullDecryptLen = Math.min(10240, data.length);
|
|
1431
|
+
for (let i = 0; i < fullDecryptLen; i++) {
|
|
1432
|
+
data[i] = data[i] ^ key[i % keyLen];
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// Decrypt every 50th byte after that
|
|
1436
|
+
for (let i = fullDecryptLen; i < data.length; i += 50) {
|
|
1437
|
+
data[i] = data[i] ^ key[i % keyLen];
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
return data.buffer;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1423
1443
|
// Auto-load PDF if config is present (injected by NodeBB plugin)
|
|
1424
1444
|
async function autoLoadSecurePDF() {
|
|
1425
1445
|
if (!window.PDF_SECURE_CONFIG || !window.PDF_SECURE_CONFIG.filename) {
|
|
@@ -1457,7 +1477,7 @@
|
|
|
1457
1477
|
const nonceData = await nonceRes.json();
|
|
1458
1478
|
const nonce = nonceData.response.nonce;
|
|
1459
1479
|
|
|
1460
|
-
// Step 2: Fetch PDF binary
|
|
1480
|
+
// Step 2: Fetch encrypted PDF binary
|
|
1461
1481
|
const pdfUrl = config.relativePath + '/api/v3/plugins/pdf-secure/pdf-data?nonce=' + encodeURIComponent(nonce);
|
|
1462
1482
|
const pdfRes = await fetch(pdfUrl, { credentials: 'same-origin' });
|
|
1463
1483
|
|
|
@@ -1465,12 +1485,33 @@
|
|
|
1465
1485
|
throw new Error('PDF yüklenemedi (' + pdfRes.status + ')');
|
|
1466
1486
|
}
|
|
1467
1487
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1488
|
+
// Get XOR key from header
|
|
1489
|
+
const xorKey = pdfRes.headers.get('X-PDF-Key');
|
|
1490
|
+
const encodedBuffer = await pdfRes.arrayBuffer();
|
|
1470
1491
|
|
|
1471
|
-
|
|
1492
|
+
console.log('[PDF-Secure] Encrypted data received:', encodedBuffer.byteLength, 'bytes');
|
|
1493
|
+
|
|
1494
|
+
// Step 3: Decode XOR encrypted data
|
|
1495
|
+
let pdfBuffer;
|
|
1496
|
+
if (xorKey) {
|
|
1497
|
+
console.log('[PDF-Secure] Decoding XOR encrypted data...');
|
|
1498
|
+
pdfBuffer = partialXorDecode(encodedBuffer, xorKey);
|
|
1499
|
+
} else {
|
|
1500
|
+
// Fallback for backward compatibility
|
|
1501
|
+
pdfBuffer = encodedBuffer;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
console.log('[PDF-Secure] PDF decoded successfully');
|
|
1505
|
+
|
|
1506
|
+
// Step 4: Load into viewer
|
|
1472
1507
|
await loadPDFFromBuffer(pdfBuffer);
|
|
1473
1508
|
|
|
1509
|
+
// Step 5: Security - clear references to prevent extraction
|
|
1510
|
+
// Nullify the buffer after loading
|
|
1511
|
+
pdfBuffer = null;
|
|
1512
|
+
|
|
1513
|
+
console.log('[PDF-Secure] Buffer references cleared');
|
|
1514
|
+
|
|
1474
1515
|
} catch (err) {
|
|
1475
1516
|
console.error('[PDF-Secure] Auto-load error:', err);
|
|
1476
1517
|
if (dropzone) {
|