fix(pdf): make encrypt/decrypt/permissions fail honestly instead of silent no-op

This commit is contained in:
2026-08-23 19:31:33 +05:30
parent c6ec1cef64
commit 25dcaaa816
6 changed files with 187 additions and 5 deletions
+6
View File
@@ -4760,6 +4760,12 @@ ipcMain.on('process-pdf-operation', async (event, data) => {
});
}
});
// Reports pdf-lib's encryption capability (Task 27) so the renderer can
// disable the password-protection controls instead of letting the user
// fill the form only to see the operation fail.
ipcMain.handle('get-pdf-capabilities', async () => ({
passwordProtection: await PDFOperations.pdfEncryptionSupported,
}));
ipcMain.on('get-pdf-page-count', async (event, filePath) => {
try {
const count = await PDFOperations.getPageCount(filePath);
+4 -4
View File
@@ -22,10 +22,10 @@
* cannot supply (merge takes many inputs in one op; reorder needs each
* file's full page order; fillForm's field values differ per file).
* - formFields — a read-only query returning data, not a transform.
* - encrypt / decrypt / permissions — pdf-lib 1.17.1 (bundled) silently
* ignores encryption options in save() and cannot decrypt on load, so a
* batch run would either write unprotected files while reporting success
* or deterministically fail every file.
* - encrypt / decrypt / permissions — pdf-lib 1.17.1 (bundled) lacks
* encryption support, so since Task 27 these ops fail honestly with an
* "unavailable" result instead of silently writing unprotected files;
* a batch run would deterministically fail every file.
*
* @module PDFBatchOperations
*/
+35
View File
@@ -2,6 +2,30 @@ const fs = require('fs');
const path = require('path');
const { PDFDocument, rgb, degrees, StandardFonts } = require('pdf-lib');
// pdf-lib 1.17.1 cannot encrypt: SaveOptions has no userPassword/ownerPassword/
// permissions fields, so save() silently ignores them and writes an unprotected
// file, and PDFDocument.load() cannot open password-protected input (verified
// empirically in Task 22's review). Rather than trusting a pinned version
// string, probe the installed library once at module load: save a tiny
// in-memory document with a userPassword and check the raw bytes for an
// /Encrypt dictionary (which an unencrypted document never contains). A library
// that supports encryption passes the probe and the password ops re-enable
// automatically. Probe errors fail closed (treated as unsupported).
const pdfEncryptionSupported = (async () => {
try {
const probeDoc = await PDFDocument.create();
const probeBytes = await probeDoc.save({ userPassword: 'encryption-capability-probe' });
return Buffer.from(probeBytes).includes('/Encrypt');
} catch {
return false;
}
})();
// Returned by the password ops when the probe reports no encryption support
// (Task 27): fail honestly instead of silently writing an unprotected file.
const PDF_ENCRYPTION_UNAVAILABLE_MESSAGE =
'Password protection is not available in this build (pdf-lib lacks encryption support).';
function parsePageRanges(rangeString, totalPages) {
const pages = [];
const ranges = rangeString.split(',').map((r) => r.trim());
@@ -301,6 +325,9 @@ async function pdfWatermark(data) {
}
async function pdfEncrypt(data) {
if (!(await pdfEncryptionSupported)) {
return { success: false, message: PDF_ENCRYPTION_UNAVAILABLE_MESSAGE };
}
try {
const pdfBytes = fs.readFileSync(data.inputPath);
const pdf = await PDFDocument.load(pdfBytes);
@@ -335,6 +362,9 @@ async function pdfEncrypt(data) {
}
async function pdfDecrypt(data) {
if (!(await pdfEncryptionSupported)) {
return { success: false, message: PDF_ENCRYPTION_UNAVAILABLE_MESSAGE };
}
try {
const pdfBytes = fs.readFileSync(data.inputPath);
const pdf = await PDFDocument.load(pdfBytes, { password: data.password });
@@ -352,6 +382,9 @@ async function pdfDecrypt(data) {
}
async function pdfSetPermissions(data) {
if (!(await pdfEncryptionSupported)) {
return { success: false, message: PDF_ENCRYPTION_UNAVAILABLE_MESSAGE };
}
try {
const pdfBytes = fs.readFileSync(data.inputPath);
const loadOptions = data.currentPassword ? { password: data.currentPassword } : {};
@@ -701,6 +734,8 @@ async function getPageCount(filePath) {
module.exports = {
parsePageRanges,
hexToRgb,
pdfEncryptionSupported,
PDF_ENCRYPTION_UNAVAILABLE_MESSAGE,
pdfMerge,
pdfSplit,
pdfCompress,
+1
View File
@@ -86,6 +86,7 @@ const ALLOWED_SEND_CHANNELS = [
// PDF operations
'process-pdf-operation',
'get-pdf-page-count',
'get-pdf-capabilities',
'select-pdf-folder',
'batch-pdf-operation',
+34 -1
View File
@@ -3922,8 +3922,41 @@ function updateMergeFilesList() {
});
}
// Task 27: pdf-lib 1.17.1 cannot encrypt, so encrypt/decrypt/permissions fail
// honestly in the main process. Ask main once whether password protection is
// available and, when it is not, disable the three sections' controls with an
// explanatory hint instead of letting the user fill the form only to see the
// operation fail. If the capability query itself fails, leave the controls
// enabled — main still fails the operation honestly on submit.
async function applyPDFPasswordProtectionAvailability() {
let capabilities;
try {
capabilities = await ipcRenderer.invoke('get-pdf-capabilities');
} catch {
return;
}
if (!capabilities || capabilities.passwordProtection !== false) return;
const sectionIds = ['pdf-encrypt-section', 'pdf-decrypt-section', 'pdf-permissions-section'];
for (const sectionId of sectionIds) {
const section = document.getElementById(sectionId);
if (!section) continue;
section.querySelectorAll('input, select, button').forEach((control) => {
control.disabled = true;
});
const hint = document.createElement('p');
hint.className = 'warning-message pdf-unavailable-hint';
hint.textContent =
'Password protection is not available in this build (pdf-lib lacks encryption support).';
section.prepend(hint);
}
}
// PDF Editor Event Listeners
document.addEventListener('DOMContentLoaded', () => {
// Disable password-protection controls when pdf-lib lacks encryption support
applyPDFPasswordProtectionAvailability();
// Close PDF Editor Dialog
const pdfEditorClose = document.getElementById('pdf-editor-dialog-close');
if (pdfEditorClose) {
@@ -4694,7 +4727,7 @@ ipcRenderer.on('pdf-operation-complete', (event, { success, error, message }) =>
}
}, 800);
} else {
showPDFStatus(`Error: ${error || 'PDF operation failed'}`, 'warning');
showPDFStatus(`Error: ${error || message || 'PDF operation failed'}`, 'warning');
}
});