diff --git a/package-lock.json b/package-lock.json index 12c10cd..90723ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@types/pdf-parse": "^1.1.5", "@vercel/analytics": "^1.6.1", "canvas": "^3.2.1", + "cryptpdf": "^0.1.1", "docx": "^9.5.1", "file-saver": "^2.0.5", "next": "16.1.1", @@ -3823,6 +3824,18 @@ "node": ">= 8" } }, + "node_modules/cryptpdf": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/cryptpdf/-/cryptpdf-0.1.1.tgz", + "integrity": "sha512-6E/rKfYAZSz30gXgZuYXjAP6Dsi2L6jVAXrq0PYjzDDN53QrwOBNJfWbJWZWIsJ4H1xAgMokkWjVXe1MopvWhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "pdf-lib": "^1.17.1" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", diff --git a/package.json b/package.json index a5be01b..06b3d98 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@types/pdf-parse": "^1.1.5", "@vercel/analytics": "^1.6.1", "canvas": "^3.2.1", + "cryptpdf": "^0.1.1", "docx": "^9.5.1", "file-saver": "^2.0.5", "next": "16.1.1", diff --git a/src/app/api/pdf-lock/route.ts b/src/app/api/pdf-lock/route.ts index 8e27934..66ac6e0 100644 --- a/src/app/api/pdf-lock/route.ts +++ b/src/app/api/pdf-lock/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { PDFDocument } from 'pdf-lib'; +import { encryptPDF } from 'cryptpdf'; const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB @@ -48,38 +48,13 @@ export async function POST(request: NextRequest) { } const arrayBuffer = await file.arrayBuffer(); - const pdfDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true }); - - // Create a new copy of the PDF - const newPdf = await PDFDocument.create(); - const pages = await newPdf.copyPages(pdfDoc, pdfDoc.getPageIndices()); - pages.forEach((page) => newPdf.addPage(page)); - - // Copy metadata - const title = pdfDoc.getTitle(); - const author = pdfDoc.getAuthor(); - const subject = pdfDoc.getSubject(); - const creator = pdfDoc.getCreator(); - - if (title) newPdf.setTitle(title); - if (author) newPdf.setAuthor(author); - if (subject) newPdf.setSubject(subject); - if (creator) newPdf.setCreator(creator); - - newPdf.setSubject(subject || 'Password Protected Document'); - newPdf.setKeywords(['protected', 'locked']); - newPdf.setProducer('Toolverse PDF Locker'); - newPdf.setCreationDate(new Date()); - newPdf.setModificationDate(new Date()); - - // NOTE: pdf-lib does not support PDF encryption natively. - // The PDF is saved as an unencrypted copy with protection metadata. - // For true password-based encryption, a native library (muhammara, qpdf) is needed. - // This implementation creates a clean copy with metadata indicating protection intent. - - const pdfBytes = await newPdf.save({ - useObjectStreams: true, - }); + const effectiveUserPassword = userPassword || ownerPassword; + const effectiveOwnerPassword = ownerPassword || userPassword; + const pdfBytes = await encryptPDF( + new Uint8Array(arrayBuffer), + effectiveUserPassword, + effectiveOwnerPassword, + ); // Parse permissions for response const permissionList = permissions ? permissions.split(',').map(p => p.trim()) : []; @@ -88,10 +63,9 @@ export async function POST(request: NextRequest) { headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': `attachment; filename="protected_${file.name}"`, - 'X-Page-Count': pdfDoc.getPageCount().toString(), 'X-Protection-Level': ownerPassword ? 'owner+user' : 'user', 'X-Permissions': permissionList.join(',') || 'none', - 'X-Note': 'PDF encryption requires native libraries. This creates a clean copy with protection metadata. For full encryption, use a desktop PDF tool or server-side qpdf.', + 'X-Encryption': 'AES-256', }, }); diff --git a/src/app/api/pdf-unlock/route.ts b/src/app/api/pdf-unlock/route.ts index fd0f286..f4809ac 100644 --- a/src/app/api/pdf-unlock/route.ts +++ b/src/app/api/pdf-unlock/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import { decryptPDF } from 'cryptpdf'; import { PDFDocument } from 'pdf-lib'; const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB @@ -25,20 +26,23 @@ export async function POST(request: NextRequest) { } const arrayBuffer = await file.arrayBuffer(); + let sourceBytes = new Uint8Array(arrayBuffer); - // Strategy: Try to load the PDF in progressively more permissive ways + // Strategy: Try AES-256 decryption first, then fall back to prior pdf-lib behavior let sourcePdf: PDFDocument | null = null; let unlockMethod = 'none'; - // Step 1: Try loading with the provided password (without ignoring encryption) + // Step 1: Try decrypting Toolverse-encrypted PDFs if (password) { try { - sourcePdf = await PDFDocument.load(arrayBuffer, { + const decryptedBytes = await decryptPDF(sourceBytes, password); + sourceBytes = new Uint8Array(decryptedBytes); + sourcePdf = await PDFDocument.load(sourceBytes, { ignoreEncryption: false, }); - unlockMethod = 'password'; + unlockMethod = 'aes-256-password'; } catch { - // Password didn't work or encryption type not supported + // Password didn't work or file is not in this encryption format sourcePdf = null; } } @@ -46,7 +50,7 @@ export async function POST(request: NextRequest) { // Step 2: Try loading without any password (might not be encrypted, or permissions-only lock) if (!sourcePdf) { try { - sourcePdf = await PDFDocument.load(arrayBuffer, { + sourcePdf = await PDFDocument.load(sourceBytes, { ignoreEncryption: false, }); unlockMethod = 'no-encryption'; @@ -58,7 +62,7 @@ export async function POST(request: NextRequest) { // Step 3: Fall back to ignoring encryption (strips permissions-based restrictions) if (!sourcePdf) { try { - sourcePdf = await PDFDocument.load(arrayBuffer, { + sourcePdf = await PDFDocument.load(sourceBytes, { ignoreEncryption: true, }); unlockMethod = 'bypass';