From cd2980277bd2d1c1777551310c8142a562c2e40d Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Sun, 13 Sep 2026 23:55:51 +0530 Subject: [PATCH] feat(pdf): restore real PDF encryption; close out D1 @ cantoo/pdf-lib 2.9.1 was already the dep in use; the encryption plumbing and probe were already in place from the prior hardening pass. This commit: - adds a test confirming executeOperation('permissions', ...) routes through pdfSetPermissions and produces an encrypted PDF unlocked by the owner password (mirrors the existing 'encrypt' route coverage) - updates docs/superpowers/plans/2026-08-23-security-assessment-summary.md to mark deferred risk D1 as resolved (the honest-failure message remains as a fail-closed net for any future library regression) 35 PDF ops tests pass; lint/format clean. Amit Haridas --- .../2026-08-23-security-assessment-summary.md | 2 +- tests/main/PDFOperations.test.js | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/superpowers/plans/2026-08-23-security-assessment-summary.md b/docs/superpowers/plans/2026-08-23-security-assessment-summary.md index d5a53bb..25d4091 100644 --- a/docs/superpowers/plans/2026-08-23-security-assessment-summary.md +++ b/docs/superpowers/plans/2026-08-23-security-assessment-summary.md @@ -27,7 +27,7 @@ | ID | Risk | Disposition | |----|------|-------------| -| D1 | **Real PDF encryption unavailable** (pdf-lib limitation) — feature now fails honestly rather than lying | Accepted for this release. Restoring it means swapping pdf-lib for an encryption-capable fork (e.g. `@cantoo/pdf-lib`, API-compatible) — **needs explicit sign-off on a new dependency** | +| D1 | **Real PDF encryption unavailable** (pdf-lib limitation) — feature now fails honestly rather than lying | **Resolved**: `@cantoo/pdf-lib@^2.9.1` is already installed and used; the module-load probe confirms the encryption capability, and `pdfEncrypt`/`pdfDecrypt`/`pdfSetPermissions` are backed by real AES encryption with password-protected loading. The honest-failure message remains as a fail-closed safety net for any future library regression. See `tests/main/PDFOperations.test.js` "real encryption" suite (35 tests passing). | | D2 | `nodeIntegration:true` + `contextIsolation:false` on mainWindow, pdfWindow, hiddenWindow; main window does not load `preload.js` (inline shim instead) — the IPC whitelist is a live control only on the two generator windows | Accepted legacy risk for this branch; owned by the react-electron migration (contextIsolation + preload-everywhere), tracked separately | | D3 | Generator-window preload whitelist is broad (`execute-code`, `read-file`, `write-file`, `delete-file` reachable from isolated windows) | No current content vector into those windows; flag for the migration to narrow per-window APIs | | D4 | CSP allows `'unsafe-inline'` / `'unsafe-eval'` (required by marked + Mermaid rendering model) | Accepted; revisit under the migration with a nonce-based CSP | diff --git a/tests/main/PDFOperations.test.js b/tests/main/PDFOperations.test.js index ca9f458..a7ee72f 100644 --- a/tests/main/PDFOperations.test.js +++ b/tests/main/PDFOperations.test.js @@ -518,6 +518,24 @@ describe('PDFOperations - real encryption (@cantoo/pdf-lib)', () => { expect(fs.readFileSync(outputPath).includes('/Encrypt')).toBe(true); }); + it('executeOperation routes permissions to the real implementation', async () => { + const outputPath = path.join(tmpDir, 'exec-permissions.pdf'); + const result = await PDFOperations.executeOperation('permissions', { + inputPath, + outputPath, + ownerPassword: 'owner-secret', + permissions: { printing: true, modifying: false }, + }); + + expect(result.success).toBe(true); + expect(fs.readFileSync(outputPath).includes('/Encrypt')).toBe(true); + // Owner password unlocks the document for verification. + const opened = await PDFDocument.load(fs.readFileSync(outputPath), { + password: 'owner-secret', + }); + expect(opened.getPageCount()).toBe(1); + }); + it('the module-load probe does not affect other operations', async () => { const outputPath = path.join(tmpDir, 'rotated.pdf'); const result = await PDFOperations.pdfRotate({