Handling Password-Protected Encrypted PDFs with pdf-inspector

pdf-inspector handles encrypted PDFs by accepting a password through the PdfOptions builder, automatically attempting decryption with lopdf, and falling back to an empty password for owner-only protection.

The firecrawl/pdf-inspector Rust library provides built-in support for opening and processing password-protected PDFs without requiring external preprocessing. All decryption logic is contained in the core library at src/lib.rs, making encrypted document handling transparent to downstream operations like type detection and Markdown extraction.

How Password Protection Works in pdf-inspector

The library implements a two-tiered approach to password handling: user-provided passwords take precedence, with automatic fallback to empty passwords for common owner-only encryption schemes.

The PdfOptions Builder Pattern

Password entry begins with the PdfOptions struct, which exposes an optional password field. According to the source code at src/lib.rs lines 185-188, this field carries a clear doc comment explaining the fallback behavior:

pub struct PdfOptions {
    …
    /// Password for decrypting an encrypted PDF. `None` falls back to the
    /// empty password (owner‑only encryption).
    pub password: Option<String>,
}

When password is None, the library automatically attempts decryption with an empty string—sufficient for PDFs with owner passwords but no user passwords.

Password-Aware Document Loading

All high-level API entry points in pdf-inspector—including process_pdf, process_pdf_mem, detect_pdf, and their _with_options variants—route through dedicated password-aware loader functions. These internal helpers are defined at src/lib.rs lines 75-78:

pub(crate) fn load_document_from_path_with_password<P: AsRef<Path>>(
    path: P,
    password: Option<&str>,
) -> Result<(Document, u32), PdfError> { … }

A corresponding load_document_from_mem_with_password handles in-memory byte buffers. Both functions forward the optional password to the low-level decryption routine, ensuring consistent behavior across file and memory-based workflows.

The Decryption Routine: Automatic Fallback Handling

The actual decryption logic resides in decrypt_document_bytes at src/lib.rs lines 40-48. This function implements a robust retry mechanism using the underlying lopdf crate:

fn decrypt_document_bytes(buf: &[u8], password: Option<&str>) -> Result<Document, lopdf::Error> {
    let pw = password.unwrap_or("");
    match Document::load_mem_with_options(buf, lopdf::LoadOptions::with_password(pw)) {
        Ok(doc) => Ok(doc),
        Err(inner) if !pw.is_empty() => {
            Document::load_mem_with_options(buf, lopdf::LoadOptions::with_password(""))
                .map_err(|_| inner)
        }
        Err(inner) => Err(inner),
    }
}

This implementation provides two key reliability features:

  1. Primary attempt – Tries the user-supplied password (or empty string if none provided)
  2. Automatic fallback – If the primary attempt fails with a non-empty password, retries with an empty password before returning the original error

The fallback covers PDFs encrypted with owner passwords where users legitimately lack the user password but the document permits empty-password access.

Practical Code Examples

Processing a Password-Protected File from Disk

use pdf_inspector::{process_pdf_with_options, PdfOptions};

fn main() -> Result<(), pdf_inspector::PdfError> {
    // Configure options with the decryption password
    let opts = PdfOptions::new()
        .password("my-secret-pw");

    // Process the encrypted PDF transparently
    let result = process_pdf_with_options("protected.pdf", opts)?;
    println!("PDF type: {:?}, pages: {}", result.pdf_type, result.page_count);
    
    if let Some(md) = result.markdown {
        println!("{}", md);
    }
    Ok(())
}

Processing In-Memory PDF Data

use pdf_inspector::PdfOptions;

fn process_downloaded_pdf(data: &[u8]) -> Result<(), pdf_inspector::PdfError> {
    let opts = PdfOptions::new().password("my-secret-pw");
    
    let result = pdf_inspector::process_pdf_mem_with_options(data, opts)?;
    // Same result structure as file-based processing
    println!("Detected type: {:?}", result.pdf_type);
    Ok(())
}

Both examples demonstrate that password handling does not alter the API contract—the same PdfResult type is returned regardless of encryption status.

Security Considerations

Password Redaction in Debug Output

The PdfOptions struct implements a custom Debug trait that explicitly redacts the password field. This prevents accidental credential exposure in logs, backtraces, or diagnostic output. Your passwords remain confidential even when debugging or serializing configuration objects.

No Password Persistence

pdf-inspector does not cache, store, or transmit passwords beyond the immediate decryption operation. The password exists only in memory during Document loading and is dropped immediately after.

Integration Points in the Source Code

Component File Path Role in Password Handling
Options definition src/lib.rs PdfOptions struct with password field
Password-aware loaders src/lib.rs load_document_from_path_with_password, load_document_from_mem_with_password
Decryption implementation src/lib.rs decrypt_document_bytes with lopdf integration and fallback logic
Extractor integration src/extractor/mod.rs Calls password-aware loaders before detection/extraction
Type detection src/detector.rs Operates on decrypted Document objects transparently

Summary

  • Password entry: Use PdfOptions::new().password("your-pw") before any processing call
  • Automatic fallback: pdf-inspector retries with empty passwords when initial decryption fails
  • Universal API: File-based (process_pdf_with_options) and memory-based (process_pdf_mem_with_options) functions share identical password handling
  • Security by default: Passwords are redacted from Debug output and never persisted

Frequently Asked Questions

What happens if I provide the wrong password?

pdf-inspector returns the original lopdf::Error after attempting both the provided password and an empty password fallback. The error indicates decryption failure—handle it in your application to prompt users for correct credentials.

Can pdf-inspector crack PDF passwords?

No. The library only attempts decryption with passwords you provide, plus a single empty-password fallback for owner-only protection. It does not implement brute-force, dictionary attacks, or cryptographic bypasses.

Does password protection affect Markdown extraction quality?

No. Once decrypted, the Document object passed to the extractor and detector layers is identical to a never-encrypted PDF. All downstream processing—type detection, content extraction, and Markdown generation—operates identically.

How do I handle PDFs where I don't know if they're encrypted?

Simply omit the .password() call. pdf-inspector attempts empty-password decryption automatically, which succeeds for unencrypted PDFs and owner-only encrypted PDFs. Only supply a password when you know user-password encryption is present.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →