Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
44 changes: 9 additions & 35 deletions src/app/api/pdf-lock/route.ts
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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()) : [];
Expand All @@ -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',
Comment on lines 59 to +68

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

permissions is parsed and echoed back in X-Permissions, but it is not used to configure encryptPDF(...). This makes the API response misleading (the produced PDF may not actually enforce the requested permission restrictions). Either pass the parsed permission set into the encryption call (if cryptpdf supports it) or stop accepting/returning permissions and update the client accordingly.

Copilot uses AI. Check for mistakes.
},
});

Expand Down
18 changes: 11 additions & 7 deletions src/app/api/pdf-unlock/route.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -25,28 +26,31 @@ 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;
}
}

// 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';
Expand All @@ -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';
Expand Down