nodebb-plugin-pdf-secure 1.2.0 → 1.2.2
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 +51 -66
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();
|
|
1491
|
+
|
|
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');
|
|
1470
1505
|
|
|
1471
|
-
// Load into viewer
|
|
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) {
|
|
@@ -3102,14 +3143,10 @@
|
|
|
3102
3143
|
}
|
|
3103
3144
|
}, true);
|
|
3104
3145
|
|
|
3105
|
-
// 2. Block context menu (right-click)
|
|
3146
|
+
// 2. Block context menu (right-click) - EVERYWHERE
|
|
3106
3147
|
document.addEventListener('contextmenu', function (e) {
|
|
3107
|
-
// Allow our custom context menu for annotations
|
|
3108
|
-
if (e.target.closest('.annotationLayer') || e.target.closest('.pdfViewer')) {
|
|
3109
|
-
// Keep custom context menu behavior
|
|
3110
|
-
return;
|
|
3111
|
-
}
|
|
3112
3148
|
e.preventDefault();
|
|
3149
|
+
e.stopPropagation();
|
|
3113
3150
|
return false;
|
|
3114
3151
|
}, true);
|
|
3115
3152
|
|
|
@@ -3131,41 +3168,14 @@
|
|
|
3131
3168
|
return false;
|
|
3132
3169
|
}, true);
|
|
3133
3170
|
|
|
3134
|
-
// 5.
|
|
3135
|
-
let devToolsOpen = false;
|
|
3136
|
-
const threshold = 160;
|
|
3137
|
-
|
|
3138
|
-
function checkDevTools() {
|
|
3139
|
-
const widthThreshold = window.outerWidth - window.innerWidth > threshold;
|
|
3140
|
-
const heightThreshold = window.outerHeight - window.innerHeight > threshold;
|
|
3141
|
-
|
|
3142
|
-
if (widthThreshold || heightThreshold) {
|
|
3143
|
-
if (!devToolsOpen) {
|
|
3144
|
-
devToolsOpen = true;
|
|
3145
|
-
console.log('[Security] DevTools detected');
|
|
3146
|
-
// Optional: blur content when devtools open
|
|
3147
|
-
document.body.classList.add('devtools-open');
|
|
3148
|
-
}
|
|
3149
|
-
} else {
|
|
3150
|
-
if (devToolsOpen) {
|
|
3151
|
-
devToolsOpen = false;
|
|
3152
|
-
document.body.classList.remove('devtools-open');
|
|
3153
|
-
}
|
|
3154
|
-
}
|
|
3155
|
-
}
|
|
3156
|
-
|
|
3157
|
-
// Check periodically
|
|
3158
|
-
setInterval(checkDevTools, 1000);
|
|
3159
|
-
window.addEventListener('resize', checkDevTools);
|
|
3160
|
-
|
|
3161
|
-
// 6. Block Print via window.print override
|
|
3171
|
+
// 5. Block Print via window.print override
|
|
3162
3172
|
window.print = function () {
|
|
3163
3173
|
console.log('[Security] Print function blocked');
|
|
3164
3174
|
alert('Yazdırma bu belgede engellenmiştir.');
|
|
3165
3175
|
return false;
|
|
3166
3176
|
};
|
|
3167
3177
|
|
|
3168
|
-
//
|
|
3178
|
+
// 6. Disable beforeprint event
|
|
3169
3179
|
window.addEventListener('beforeprint', function (e) {
|
|
3170
3180
|
e.preventDefault();
|
|
3171
3181
|
document.body.style.display = 'none';
|
|
@@ -3175,7 +3185,7 @@
|
|
|
3175
3185
|
document.body.style.display = '';
|
|
3176
3186
|
});
|
|
3177
3187
|
|
|
3178
|
-
//
|
|
3188
|
+
// 7. Block screenshot keyboard shortcuts
|
|
3179
3189
|
document.addEventListener('keyup', function (e) {
|
|
3180
3190
|
// PrintScreen key
|
|
3181
3191
|
if (e.key === 'PrintScreen') {
|
|
@@ -3184,10 +3194,9 @@
|
|
|
3184
3194
|
}
|
|
3185
3195
|
}, true);
|
|
3186
3196
|
|
|
3187
|
-
//
|
|
3197
|
+
// 8. Visibility change detection (tab switching for screenshots)
|
|
3188
3198
|
document.addEventListener('visibilitychange', function () {
|
|
3189
3199
|
if (document.hidden) {
|
|
3190
|
-
// User switched tabs - could be for screenshot tools
|
|
3191
3200
|
console.log('[Security] Tab hidden');
|
|
3192
3201
|
}
|
|
3193
3202
|
});
|
|
@@ -3195,30 +3204,6 @@
|
|
|
3195
3204
|
console.log('[Security] All protection features initialized');
|
|
3196
3205
|
})();
|
|
3197
3206
|
</script>
|
|
3198
|
-
|
|
3199
|
-
<!-- Security CSS for DevTools detection -->
|
|
3200
|
-
<style>
|
|
3201
|
-
.devtools-open #viewerContainer,
|
|
3202
|
-
.devtools-open #viewer,
|
|
3203
|
-
.devtools-open .pdfViewer {
|
|
3204
|
-
filter: blur(20px) !important;
|
|
3205
|
-
pointer-events: none !important;
|
|
3206
|
-
}
|
|
3207
|
-
|
|
3208
|
-
.devtools-open::after {
|
|
3209
|
-
content: 'Geliştirici araçları açıkken içerik görüntülenemez.';
|
|
3210
|
-
position: fixed;
|
|
3211
|
-
top: 50%;
|
|
3212
|
-
left: 50%;
|
|
3213
|
-
transform: translate(-50%, -50%);
|
|
3214
|
-
background: rgba(0, 0, 0, 0.9);
|
|
3215
|
-
color: #fff;
|
|
3216
|
-
padding: 30px 50px;
|
|
3217
|
-
border-radius: 10px;
|
|
3218
|
-
font-size: 18px;
|
|
3219
|
-
z-index: 99999;
|
|
3220
|
-
}
|
|
3221
|
-
</style>
|
|
3222
3207
|
</body>
|
|
3223
3208
|
|
|
3224
3209
|
</html>
|