nodebb-plugin-pdf-secure2 1.4.2 → 1.5.0

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.
@@ -53,18 +53,18 @@ NonceStore.validate = function (nonce, uid) {
53
53
  return null;
54
54
  }
55
55
 
56
- // Delete immediately (single-use)
57
- store.delete(nonce);
58
-
59
- // Check UID match
56
+ // Validate BEFORE deleting — prevents DoS via nonce consumption with wrong UID
60
57
  if (data.uid !== uid) {
61
58
  return null;
62
59
  }
63
60
 
64
61
  // Check TTL
65
62
  if (Date.now() - data.createdAt > NONCE_TTL) {
63
+ store.delete(nonce); // Expired, clean up
66
64
  return null;
67
65
  }
68
66
 
67
+ // All checks passed — delete now (single-use)
68
+ store.delete(nonce);
69
69
  return data; // Includes encKey and encIv for AES-256-GCM
70
70
  };
@@ -36,7 +36,6 @@ PdfHandler.resolveFilePath = function (filename) {
36
36
  const uploadPath = nconf.get('upload_path') || path.join(nconf.get('base_dir'), 'public', 'uploads');
37
37
  const filePath = path.join(uploadPath, 'files', safeName);
38
38
 
39
- // Verify the resolved path is still within the upload directory
40
39
  const resolvedPath = path.resolve(filePath);
41
40
  const resolvedUploadDir = path.resolve(path.join(uploadPath, 'files'));
42
41
  if (!resolvedPath.startsWith(resolvedUploadDir)) {
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ const privileges = require.main.require('./src/privileges');
4
+ const topics = require.main.require('./src/topics');
5
+ const posts = require.main.require('./src/posts');
6
+ const groups = require.main.require('./src/groups');
7
+ const db = require.main.require('./src/database');
8
+
9
+ const TopicAccess = module.exports;
10
+
11
+ /**
12
+ * Validate that a user has access to a PDF through a specific topic.
13
+ * Checks: 1) User can read the topic, 2) The PDF filename exists in the topic's posts.
14
+ * Admin/Global Moderators bypass all checks.
15
+ *
16
+ * @param {number} uid - User ID
17
+ * @param {number|string} tid - Topic ID
18
+ * @param {string} filename - Sanitized PDF filename (basename only)
19
+ * @returns {Promise<{allowed: boolean, reason?: string}>}
20
+ */
21
+ TopicAccess.validate = async function (uid, tid, filename) {
22
+ // Require valid tid
23
+ tid = parseInt(tid, 10);
24
+ if (!tid || isNaN(tid) || tid <= 0) {
25
+ return { allowed: false, reason: 'Missing or invalid topic ID' };
26
+ }
27
+
28
+ try {
29
+ // Admin/Global Moderator bypass
30
+ const [isAdmin, isGlobalMod] = await Promise.all([
31
+ groups.isMember(uid, 'administrators'),
32
+ groups.isMember(uid, 'Global Moderators'),
33
+ ]);
34
+ if (isAdmin || isGlobalMod) {
35
+ return { allowed: true };
36
+ }
37
+
38
+ // Check if user can read the topic (NodeBB privilege system)
39
+ const canRead = await privileges.topics.can('topics:read', tid, uid);
40
+ if (!canRead) {
41
+ return { allowed: false, reason: 'Access denied' };
42
+ }
43
+
44
+ // Verify the PDF filename exists in one of the topic's posts
45
+ const exists = await TopicAccess.pdfExistsInTopic(tid, filename);
46
+ if (!exists) {
47
+ return { allowed: false, reason: 'Access denied' };
48
+ }
49
+
50
+ return { allowed: true };
51
+ } catch (err) {
52
+ // DB error, topic not found, etc. — deny by default
53
+ return { allowed: false, reason: 'Access check failed' };
54
+ }
55
+ };
56
+
57
+ /**
58
+ * Check if a PDF filename is referenced in any post of a topic.
59
+ * Searches both the main post and all reply posts.
60
+ *
61
+ * @param {number} tid - Topic ID
62
+ * @param {string} filename - PDF filename to search for
63
+ * @returns {Promise<boolean>}
64
+ */
65
+ TopicAccess.pdfExistsInTopic = async function (tid, filename) {
66
+ // Get the topic's main post ID
67
+ const topicData = await topics.getTopicFields(tid, ['mainPid']);
68
+ if (!topicData || !topicData.mainPid) {
69
+ return false;
70
+ }
71
+
72
+ // Get all post IDs in this topic (replies)
73
+ const replyPids = await db.getSortedSetRange('tid:' + tid + ':posts', 0, -1);
74
+ const allPids = [topicData.mainPid, ...replyPids].filter(Boolean);
75
+
76
+ if (allPids.length === 0) {
77
+ return false;
78
+ }
79
+
80
+ // Get raw content of all posts
81
+ const postsData = await posts.getPostsFields(allPids, ['content']);
82
+
83
+ // Escape filename for regex safety, also match URL-encoded variant
84
+ // (post content may store "Özel Döküman.pdf" as "%C3%96zel%20D%C3%B6k%C3%BCman.pdf")
85
+ const escaped = filename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
86
+ const encodedEscaped = encodeURIComponent(filename).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
87
+ const pattern = new RegExp('(' + escaped + '|' + encodedEscaped + ')', 'i');
88
+
89
+ for (const post of postsData) {
90
+ if (post && post.content && pattern.test(post.content)) {
91
+ return true;
92
+ }
93
+ }
94
+
95
+ return false;
96
+ };
package/library.js CHANGED
@@ -11,6 +11,7 @@ const controllers = require('./lib/controllers');
11
11
  const nonceStore = require('./lib/nonce-store');
12
12
  const pdfHandler = require('./lib/pdf-handler');
13
13
  const geminiChat = require('./lib/gemini-chat');
14
+ const topicAccess = require('./lib/topic-access');
14
15
 
15
16
  const plugin = {};
16
17
 
@@ -18,6 +19,16 @@ const plugin = {};
18
19
  let viewerHtmlCache = null;
19
20
  let pluginSettings = {};
20
21
 
22
+ // Rate limit for viewer endpoint — prevents brute-force topic ID enumeration
23
+ const viewerRateLimit = new Map(); // uid -> { count, windowStart }
24
+ const VIEWER_RATE_LIMIT = { max: 15, window: 60 * 1000 }; // 15 requests per 60 seconds
25
+ setInterval(() => {
26
+ const cutoff = Date.now() - VIEWER_RATE_LIMIT.window;
27
+ for (const [uid, data] of viewerRateLimit.entries()) {
28
+ if (data.windowStart < cutoff) viewerRateLimit.delete(uid);
29
+ }
30
+ }, 60 * 1000).unref();
31
+
21
32
  plugin.init = async (params) => {
22
33
  const { router, middleware } = params;
23
34
 
@@ -26,6 +37,7 @@ plugin.init = async (params) => {
26
37
  try {
27
38
  viewerHtmlCache = fs.readFileSync(viewerPath, 'utf8');
28
39
  } catch (err) {
40
+ console.error('[PDF-Secure] Failed to read viewer.html:', err.message);
29
41
  }
30
42
 
31
43
  // Double slash bypass protection - catches /uploads//files/ attempts
@@ -76,6 +88,12 @@ plugin.init = async (params) => {
76
88
  // Apply admin-configured quota settings
77
89
  controllers.initQuotaSettings(pluginSettings);
78
90
 
91
+ // Apply admin-configured custom prompts
92
+ geminiChat.setCustomPrompts({
93
+ premium: pluginSettings.promptPremium || '',
94
+ vip: pluginSettings.promptVip || '',
95
+ });
96
+
79
97
  const watermarkEnabled = pluginSettings.watermarkEnabled === 'on';
80
98
 
81
99
  // Admin page route
@@ -88,6 +106,19 @@ plugin.init = async (params) => {
88
106
  return res.status(401).json({ error: 'Authentication required' });
89
107
  }
90
108
 
109
+ // Rate limit — prevent brute-force topic ID enumeration
110
+ const now = Date.now();
111
+ const rateData = viewerRateLimit.get(req.uid) || { count: 0, windowStart: now };
112
+ if (now - rateData.windowStart > VIEWER_RATE_LIMIT.window) {
113
+ rateData.count = 0;
114
+ rateData.windowStart = now;
115
+ }
116
+ rateData.count++;
117
+ viewerRateLimit.set(req.uid, rateData);
118
+ if (rateData.count > VIEWER_RATE_LIMIT.max) {
119
+ return res.status(429).json({ error: 'Too many requests. Please slow down.' });
120
+ }
121
+
91
122
  const { file } = req.query;
92
123
  if (!file) {
93
124
  return res.status(400).send('Missing file parameter');
@@ -99,6 +130,13 @@ plugin.init = async (params) => {
99
130
  return res.status(400).send('Invalid file');
100
131
  }
101
132
 
133
+ // Topic-level access control: require tid and validate access
134
+ const { tid } = req.query;
135
+ const accessResult = await topicAccess.validate(req.uid, tid, safeName);
136
+ if (!accessResult.allowed) {
137
+ return res.status(403).json({ error: accessResult.reason || 'Access denied' });
138
+ }
139
+
102
140
  // Check cache
103
141
  if (!viewerHtmlCache) {
104
142
  return res.status(500).send('Viewer not available');
@@ -132,6 +170,7 @@ plugin.init = async (params) => {
132
170
  try {
133
171
  totalPages = await pdfHandler.getTotalPages(safeName);
134
172
  } catch (err) {
173
+ console.error('[PDF-Secure] Failed to get total pages for', safeName, ':', err.message);
135
174
  }
136
175
  }
137
176
 
@@ -152,6 +191,7 @@ plugin.init = async (params) => {
152
191
  // Key is embedded in HTML - NOT visible in any network API response!
153
192
  const configObj = {
154
193
  filename: safeName,
194
+ tid: parseInt(tid, 10) || 0,
155
195
  relativePath: req.app.get('relative_path') || '',
156
196
  csrfToken: req.csrfToken ? req.csrfToken() : '',
157
197
  nonce: nonceData.nonce,
@@ -187,7 +227,7 @@ plugin.init = async (params) => {
187
227
  .replace(/<script>(\r?\n\s*\/\/ IIFE to prevent global access)/, `<script nonce="${cspNonce}">$1`);
188
228
 
189
229
  // Update CSP header with the nonce for the inline viewer script
190
- res.set('Content-Security-Policy', `default-src 'self'; script-src 'self' 'unsafe-eval' 'nonce-${cspNonce}' https://cdnjs.cloudflare.com; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; img-src 'self' data: blob: https://cdnjs.cloudflare.com https://i.ibb.co; connect-src 'self'; frame-ancestors 'self'; form-action 'none'; base-uri 'self'`);
230
+ res.set('Content-Security-Policy', `default-src 'self'; script-src 'self' 'unsafe-eval' 'nonce-${cspNonce}' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; img-src 'self' data: blob: https://cdnjs.cloudflare.com https://i.ibb.co; connect-src 'self'; frame-ancestors 'self'; form-action 'none'; base-uri 'self'`);
191
231
 
192
232
  res.type('html').send(injectedHtml);
193
233
  });
@@ -257,19 +297,32 @@ plugin.filterConfig = async function (data) {
257
297
 
258
298
  // Transform PDF links to secure placeholders (server-side)
259
299
  // This hides PDF URLs from: page source, API, RSS, ActivityPub
300
+ // Supports both filter:parse.post (data.postData.content) and filter:parse.raw (string)
260
301
  plugin.transformPdfLinks = async (data) => {
261
- if (!data || !data.postData || !data.postData.content) {
302
+ // Support multiple hook data formats
303
+ let content = null;
304
+ let contentPath = null;
305
+
306
+ if (data && data.postData && data.postData.content) {
307
+ content = data.postData.content;
308
+ contentPath = 'postData';
309
+ } else if (data && typeof data === 'string') {
310
+ content = data;
311
+ contentPath = 'raw';
312
+ } else {
262
313
  return data;
263
314
  }
264
315
 
265
-
266
316
  // Regex to match PDF links: <a href="...xxx.pdf">text</a>
267
317
  // Captures: full URL path, filename, link text
268
318
  const pdfLinkRegex = /<a\s+[^>]*href=["']([^"']*\/([^"'\/]+\.pdf))["'][^>]*>([^<]*)<\/a>/gi;
269
319
 
270
- const matchCount = (data.postData.content.match(pdfLinkRegex) || []).length;
320
+ const matchCount = (content.match(pdfLinkRegex) || []).length;
321
+ if (matchCount === 0) {
322
+ return data;
323
+ }
271
324
 
272
- data.postData.content = data.postData.content.replace(pdfLinkRegex, (match, fullPath, filename, linkText) => {
325
+ content = content.replace(pdfLinkRegex, (match, fullPath, filename, linkText) => {
273
326
  // Decode filename to prevent double encoding (URL may already be encoded)
274
327
  let decodedFilename;
275
328
  try { decodedFilename = decodeURIComponent(filename); }
@@ -288,6 +341,13 @@ plugin.transformPdfLinks = async (data) => {
288
341
  </div>`;
289
342
  });
290
343
 
344
+ // Write back to the correct location
345
+ if (contentPath === 'postData') {
346
+ data.postData.content = content;
347
+ } else if (contentPath === 'raw') {
348
+ data = content;
349
+ }
350
+
291
351
  return data;
292
352
  };
293
353
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-pdf-secure2",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "Secure PDF viewer plugin for NodeBB - prevents downloading, enables canvas-only rendering with Premium group support",
5
5
  "main": "library.js",
6
6
  "repository": {
package/plugin.json CHANGED
@@ -26,6 +26,10 @@
26
26
  {
27
27
  "hook": "filter:parse.post",
28
28
  "method": "transformPdfLinks"
29
+ },
30
+ {
31
+ "hook": "filter:parse.raw",
32
+ "method": "transformPdfLinks"
29
33
  }
30
34
  ],
31
35
  "staticDirs": {
@@ -40,32 +40,6 @@
40
40
  let isLoading = false;
41
41
  let currentResolver = null;
42
42
 
43
- // ============================================
44
- // SPA MEMORY CACHE - Cache decoded PDF buffers
45
- // ============================================
46
- const pdfBufferCache = new Map(); // filename -> { buffer: ArrayBuffer, cachedAt: number }
47
- const CACHE_MAX_SIZE = 5; // ~50MB limit (avg 10MB per PDF)
48
- const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
49
-
50
- function setCachedBuffer(filename, buffer) {
51
- // Evict oldest if cache is full
52
- if (pdfBufferCache.size >= CACHE_MAX_SIZE) {
53
- const firstKey = pdfBufferCache.keys().next().value;
54
- pdfBufferCache.delete(firstKey);
55
- }
56
- pdfBufferCache.set(filename, { buffer: buffer, cachedAt: Date.now() });
57
- }
58
-
59
- function getCachedBuffer(filename) {
60
- const entry = pdfBufferCache.get(filename);
61
- if (!entry) return null;
62
- if (Date.now() - entry.cachedAt > CACHE_TTL) {
63
- pdfBufferCache.delete(filename);
64
- return null;
65
- }
66
- return entry.buffer;
67
- }
68
-
69
43
  // Listen for postMessage from iframe
70
44
  window.addEventListener('message', function (event) {
71
45
  // Security: Only accept messages from same origin
@@ -79,21 +53,6 @@
79
53
  }
80
54
  }
81
55
 
82
- // PDF buffer from viewer - cache it
83
- if (event.data && event.data.type === 'pdf-secure-buffer') {
84
- // Source verification: only accept buffers from pdf-secure iframes
85
- var isFromSecureIframe = false;
86
- document.querySelectorAll('.pdf-secure-iframe').forEach(function (f) {
87
- if (f.contentWindow === event.source) isFromSecureIframe = true;
88
- });
89
- if (!isFromSecureIframe) return;
90
-
91
- const { filename, buffer } = event.data;
92
- if (filename && buffer) {
93
- setCachedBuffer(filename, buffer);
94
- }
95
- }
96
-
97
56
  // Fullscreen toggle request from iframe viewer
98
57
  if (event.data && event.data.type === 'pdf-secure-fullscreen-toggle') {
99
58
  var sourceIframe = document.querySelector('.pdf-secure-iframe');
@@ -135,35 +94,6 @@
135
94
  }
136
95
  }
137
96
 
138
- // Viewer asking for cached buffer
139
- if (event.data && event.data.type === 'pdf-secure-cache-request') {
140
- // Source verification: only respond to pdf-secure iframes
141
- var isFromSecureIframe = false;
142
- document.querySelectorAll('.pdf-secure-iframe').forEach(function (f) {
143
- if (f.contentWindow === event.source) isFromSecureIframe = true;
144
- });
145
- if (!isFromSecureIframe) return;
146
-
147
- const { filename } = event.data;
148
- const cached = getCachedBuffer(filename);
149
- if (cached && event.source) {
150
- // Send cached buffer to viewer (transferable for 0-copy)
151
- // Clone once: keep original in cache, transfer the copy
152
- const copy = cached.slice(0);
153
- event.source.postMessage({
154
- type: 'pdf-secure-cache-response',
155
- filename: filename,
156
- buffer: copy
157
- }, event.origin, [copy]);
158
- } else if (event.source) {
159
- // No cache, viewer will fetch normally
160
- event.source.postMessage({
161
- type: 'pdf-secure-cache-response',
162
- filename: filename,
163
- buffer: null
164
- }, event.origin);
165
- }
166
- }
167
97
  });
168
98
 
169
99
  // Forward fullscreen state changes to all viewer iframes
@@ -300,8 +230,6 @@
300
230
  loadQueue.length = 0;
301
231
  isLoading = false;
302
232
  currentResolver = null;
303
- // Clear decrypted PDF buffer cache on navigation
304
- pdfBufferCache.clear();
305
233
  // Exit simulated fullscreen on SPA navigation
306
234
  exitSimulatedFullscreen();
307
235
  interceptPdfLinks();
@@ -453,10 +381,11 @@
453
381
  var iframe = document.createElement('iframe');
454
382
  iframe.className = 'pdf-secure-iframe';
455
383
  iframe.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;border:none;z-index:1;';
456
- iframe.src = config.relative_path + '/plugins/pdf-secure/viewer?file=' + encodeURIComponent(filename);
384
+ var tidParam = (ajaxify && ajaxify.data && ajaxify.data.tid) ? '&tid=' + encodeURIComponent(ajaxify.data.tid) : '';
385
+ iframe.src = config.relative_path + '/plugins/pdf-secure/viewer?file=' + encodeURIComponent(filename) + tidParam;
457
386
  iframe.setAttribute('frameborder', '0');
458
387
  iframe.setAttribute('allowfullscreen', 'true');
459
- iframe.setAttribute('allow', 'fullscreen');
388
+ iframe.setAttribute('allow', 'fullscreen; clipboard-write');
460
389
 
461
390
  // Store resolver for postMessage callback
462
391
  currentResolver = function () {
@@ -96,6 +96,27 @@
96
96
  </div>
97
97
  </div>
98
98
 
99
+ <hr class="my-3">
100
+ <h6 class="fw-semibold mb-3" style="font-size:13px;">AI Sistem Prompt'lari</h6>
101
+
102
+ <div class="mb-3">
103
+ <label class="form-label fw-medium" for="promptPremium" style="font-size:13px;">
104
+ <span class="badge text-bg-primary me-1" style="font-size:9px;">PREMIUM</span>
105
+ Sistem Prompt
106
+ </label>
107
+ <textarea id="promptPremium" name="promptPremium" data-key="promptPremium" class="form-control" rows="6" style="font-size:12px;" placeholder="Varsayilan prompt kullanilir..."></textarea>
108
+ <div class="form-text" style="font-size:11px;">Bos birakilirsa varsayilan prompt kullanilir. Guvenlik kurallari otomatik eklenir.</div>
109
+ </div>
110
+
111
+ <div class="mb-3">
112
+ <label class="form-label fw-medium" for="promptVip" style="font-size:13px;">
113
+ <span class="badge me-1" style="font-size:9px;background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff;">VIP</span>
114
+ Sistem Prompt
115
+ </label>
116
+ <textarea id="promptVip" name="promptVip" data-key="promptVip" class="form-control" rows="8" style="font-size:12px;" placeholder="Varsayilan prompt kullanilir..."></textarea>
117
+ <div class="form-text" style="font-size:11px;">Bos birakilirsa varsayilan prompt kullanilir. Guvenlik kurallari otomatik eklenir.</div>
118
+ </div>
119
+
99
120
  <hr class="my-3">
100
121
  <h6 class="fw-semibold mb-3" style="font-size:13px;">Token Kota Ayarlari</h6>
101
122
 
@@ -353,73 +353,29 @@
353
353
  }
354
354
 
355
355
  try {
356
- // ============================================
357
- // SPA CACHE - Check if parent has cached buffer
358
- // ============================================
359
- let pdfBuffer = null;
356
+ // Nonce and key are embedded in HTML config (not fetched from API)
357
+ const nonce = config.nonce;
358
+ const decryptKey = config.dk;
359
+ const decryptIv = config.iv;
360
360
 
361
- if (window.parent && window.parent !== window) {
362
- // Request cached buffer from parent
363
- const cachePromise = new Promise((resolve) => {
364
- const handler = (event) => {
365
- if (event.data && event.data.type === 'pdf-secure-cache-response' && event.data.filename === config.filename) {
366
- window.removeEventListener('message', handler);
367
- resolve(event.data.buffer);
368
- }
369
- };
370
- window.addEventListener('message', handler);
371
-
372
- // Timeout after 100ms
373
- setTimeout(() => {
374
- window.removeEventListener('message', handler);
375
- resolve(null);
376
- }, 100);
361
+ // Fetch encrypted PDF binary
362
+ const pdfUrl = config.relativePath + '/api/v3/plugins/pdf-secure/pdf-data?nonce=' + encodeURIComponent(nonce);
363
+ const pdfRes = await fetch(pdfUrl, { credentials: 'same-origin' });
377
364
 
378
- window.parent.postMessage({ type: 'pdf-secure-cache-request', filename: config.filename }, window.location.origin);
379
- });
380
-
381
- pdfBuffer = await cachePromise;
382
- if (pdfBuffer) {
383
- console.log('[PDF-Secure] Using cached buffer');
384
- }
365
+ if (!pdfRes.ok) {
366
+ throw new Error('PDF yüklenemedi (' + pdfRes.status + ')');
385
367
  }
386
368
 
387
- // If no cache, fetch from server
388
- if (!pdfBuffer) {
389
- // Nonce and key are embedded in HTML config (not fetched from API)
390
- const nonce = config.nonce;
391
- const decryptKey = config.dk;
392
- const decryptIv = config.iv;
393
-
394
- // Fetch encrypted PDF binary
395
- const pdfUrl = config.relativePath + '/api/v3/plugins/pdf-secure/pdf-data?nonce=' + encodeURIComponent(nonce);
396
- const pdfRes = await fetch(pdfUrl, { credentials: 'same-origin' });
369
+ const encodedBuffer = await pdfRes.arrayBuffer();
370
+ console.log('[PDF-Secure] Encrypted data received:', encodedBuffer.byteLength, 'bytes');
397
371
 
398
- if (!pdfRes.ok) {
399
- throw new Error('PDF yüklenemedi (' + pdfRes.status + ')');
400
- }
401
-
402
- const encodedBuffer = await pdfRes.arrayBuffer();
403
- console.log('[PDF-Secure] Encrypted data received:', encodedBuffer.byteLength, 'bytes');
404
-
405
- // Decrypt AES-256-GCM encrypted data
406
- if (decryptKey && decryptIv) {
407
- console.log('[PDF-Secure] Decrypting AES-256-GCM data...');
408
- pdfBuffer = await aesGcmDecode(encodedBuffer, decryptKey, decryptIv);
409
- } else {
410
- pdfBuffer = encodedBuffer;
411
- }
412
-
413
- // Send buffer to parent for caching (premium/lite only - non-premium must not leak decoded buffer)
414
- if ((_cfg.isPremium !== false || _cfg.isLite) && window.parent && window.parent !== window) {
415
- // Clone buffer for parent (we keep original)
416
- const bufferCopy = pdfBuffer.slice(0);
417
- window.parent.postMessage({
418
- type: 'pdf-secure-buffer',
419
- filename: config.filename,
420
- buffer: bufferCopy
421
- }, window.location.origin, [bufferCopy]); // Transferable
422
- }
372
+ // Decrypt AES-256-GCM encrypted data
373
+ let pdfBuffer;
374
+ if (decryptKey && decryptIv) {
375
+ console.log('[PDF-Secure] Decrypting AES-256-GCM data...');
376
+ pdfBuffer = await aesGcmDecode(encodedBuffer, decryptKey, decryptIv);
377
+ } else {
378
+ pdfBuffer = encodedBuffer;
423
379
  }
424
380
 
425
381
  console.log('[PDF-Secure] PDF decoded successfully');