# How pdf-inspector Manages Encrypted and Password-Protected PDF Documents

> Learn how pdf-inspector manages encrypted and password-protected PDFs. Discover its secure handling of passwords, automatic fallbacks, and redacted logging for enhanced security.

- Repository: [Firecrawl/pdf-inspector](https://github.com/firecrawl/pdf-inspector)
- Tags: how-to-guide
- Published: 2026-08-14

---

**pdf-inspector handles encrypted PDFs through an optional password field in `PdfOptions`, with automatic fallback to empty passwords for owner-only encryption and redacted logging to prevent credential leakage.**

The `firecrawl/pdf-inspector` crate provides transparent encryption support across all its interfaces—Rust library, CLI, and WebAssembly—without complicating the API for unencrypted documents. The implementation in [`src/lib.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/lib.rs) relies on the **`lopdf`** crate for low-level PDF operations, wrapping its decryption capabilities in a consistent, secure abstraction.

## How Password Protection Flows Through the API

### The PdfOptions Builder Pattern

Encryption handling starts with the **`PdfOptions`** struct defined in [`src/lib.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/lib.rs). The builder exposes a `password(..)` method that stores the supplied string in an `Option<String>` field:

```rust
// src/lib.rs#L85-L99
let opts = PdfOptions::new().password("secret123");

```

The custom **`Debug`** implementation redacts the password value, displaying `"[REDACTED]"` instead of the actual string. This prevents accidental credential exposure in logs, panic messages, or debugging output.

### Document Loading with Optional Decryption

All public entry points—`process_pdf`, `process_pdf_with_options`, and `process_pdf_mem_with_options`—forward the optional password to internal loaders:

- `load_document_from_path_with_password` (path-based, `src/lib.rs#L24-L30`)
- `load_document_from_mem_with_password` (memory-based, `src/lib.rs#L38-L46`)

The path-based loader reads the file into a buffer, then delegates to the memory loader, ensuring both paths share identical decryption logic.

## The Decryption Pipeline in Detail

### Step 1: Initial Parse Attempt

The memory loader first applies `fix_structure_tree_names` to repair malformed structure trees, then attempts to parse bytes via `load_document_bytes`:

```rust
// src/lib.rs#L73-L82
fn load_document_bytes(
    bytes: &[u8],
    password: Option<&str>,
) -> Result<Document, PdfError> {
    match Document::load_mem(bytes) {
        Ok(doc) if !doc.is_encrypted() => Ok(doc),
        Ok(_) | Err(_) => decrypt_document_bytes(bytes, password),
    }
}

```

If `lopdf` reports encryption—either through `is_encrypted()` or an encryption-related error—the code falls back to explicit decryption.

### Step 2: Decryption with Fallback

The **`decrypt_document_bytes`** function (`src/lib.rs#L86-L96`) implements a two-tier attempt:

1. **Primary attempt**: Uses the user-supplied password via `lopdf::LoadOptions::with_password(pw)`
2. **Fallback**: If the primary fails **and** a non-empty password was provided, retries with an empty password (handling owner-only encryption where the owner password is empty)

```rust
// Conceptual flow based on src/lib.rs#L86-L96
fn decrypt_document_bytes(bytes: &[u8], password: Option<&str>) -> Result<Document, PdfError> {
    if let Some(pw) = password {
        // Try provided password
        if let Ok(doc) = Document::load_mem(bytes, LoadOptions::with_password(pw)) {
            return Ok(doc);
        }
        // Fallback to empty owner password
        if let Ok(doc) = Document::load_mem(bytes, LoadOptions::with_password("")) {
            return Ok(doc);
        }
    }
    // Final error propagation
    Err(PdfError::Encrypted)
}

```

This graceful fallback eliminates a common user friction point—PDFs encrypted with owner-only protection that appear to require no password.

### Step 3: Error Propagation

Failed decryption bubbles up as **`PdfError::Encrypted`** or the underlying `lopdf::Error`, allowing callers to implement retry logic or credential prompts.

## Cross-Platform Interface Consistency

### Command-Line Interface

The **`pdf2md`** binary (`src/bin/pdf2md.rs#L242-L263`) exposes a `--password` flag:

```bash
pdf2md --input protected.pdf --output out.md --password secret123

```

The parsed `Option<&str>` flows into `extract_text_with_positions_pages_with_password`, reusing the same internal path as the library API.

### WebAssembly Bindings

The WASM wrapper (`wasm/src/lib.rs#L71-L84`) mirrors this behavior. The exported `detectPdf` function accepts an options object with an optional `password` field:

```javascript
import { detectPdf } from "pdf-inspector-wasm";

const result = await detectPdf(pdfBytes, { password: "secret123" });

```

## Verification Through Testing

The integration test suite validates all encryption scenarios in `tests/integration_tests.rs#L4246-L4265`:

- Rejection of encrypted PDFs without passwords
- Failure on incorrect passwords
- Successful decryption and extraction with correct passwords

## Complete Working Examples

### Rust Library

```rust
use pdf_inspector::{process_pdf_with_options, PdfOptions};

fn main() -> Result<(), pdf_inspector::PdfError> {
    // Open with password
    let opts = PdfOptions::new().password("secret123");
    let result = process_pdf_with_options("encrypted.pdf", opts)?;
    
    // Owner-only encryption (empty password fallback)
    let opts = PdfOptions::new().password("user_password");
    let result = process_pdf_with_options("owner_locked.pdf", opts)?;
    
    Ok(())
}

```

### CLI with Empty Password Flag

```bash

# Explicit empty password for owner-only encryption

pdf2md --input protected.pdf --password "" --output out.md

```

### WebAssembly in Browser

```javascript
const response = await fetch("protected.pdf");
const bytes = new Uint8Array(await response.arrayBuffer());

const result = await detectPdf(bytes, {
    password: "secret123"  // omit for unencrypted PDFs
});

```

## Security and Design Decisions

| Aspect | Implementation | Location |
|--------|---------------|----------|
| Password redaction | Custom `Debug` impl shows `"[REDACTED]"` | `src/lib.rs#L85-L99` |
| Owner-password fallback | Empty string retry after failed attempt | `src/lib.rs#L86-L96` |
| Unified decryption path | All interfaces use `load_document_*_with_password` | `src/lib.rs#L24-L46` |
| No password persistence | `Option<&str>` borrows, never stores permanently | Throughout API |

## Summary

- **Transparent handling**: Encryption is optional in `PdfOptions`; unencrypted PDFs pass through unchanged
- **Automatic fallback**: Failed password attempts retry with empty strings for owner-only protection
- **Credential protection**: Debug output never exposes passwords
- **Unified implementation**: Path, memory, CLI, and WASM interfaces share [`src/lib.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/lib.rs) core logic
- **Clear error signals**: `PdfError::Encrypted` enables caller-side retry strategies

## Frequently Asked Questions

### What happens if I don't provide a password for an encrypted PDF?

pdf-inspector returns a `PdfError::Encrypted` error after attempting to parse the document. The error propagates from `load_document_bytes` through the public API, allowing your application to detect the condition and prompt for credentials.

### Does pdf-inspector support PDFs with owner passwords but no user passwords?

Yes. When a non-empty password fails, the library automatically retries with an empty string (`""`), handling the common case where the document is encrypted but the owner password is empty. This is implemented in `decrypt_document_bytes` at `src/lib.rs#L86-L96`.

### How does pdf-inspector prevent passwords from appearing in logs?

The `PdfOptions` struct implements a custom `Debug` trait that replaces the password field with `"[REDACTED]"`. This ensures that even with `RUST_BACKTRACE=1` or explicit debug printing, credentials remain protected.

### Can I use pdf-inspector with password-protected PDFs in a WebAssembly environment?

Yes. The WASM wrapper in `wasm/src/lib.rs#L71-L84` exposes a `password` option in the `detectPdf` function options object. The password flows through the same Rust core logic as native library calls, ensuring consistent behavior across platforms.