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
This commit is contained in:
2026-09-13 23:55:51 +05:30
parent a2455c3f8a
commit cd2980277b
2 changed files with 19 additions and 1 deletions
@@ -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 |
+18
View File
@@ -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({