﻿# 介紹好用工具：pdf-encrypt (針對 PDF 加解密與讀寫文件屬性)

我經常有一個需求，就是需要把我上課所需要的簡報轉成 PDF，還要加入防寫保護的密碼(限制編輯、防止篡改內容)。但是這個需求以前我在 Windows 的時候，有一個好用的 PDF-XChange 軟體，可以很方便地幫我修改文件屬性，還可以很快地設定密碼。但是到了 macOS 之後，我發現沒有好用的工具可以用。因此，我最近就自己寫了一套全新的 CLI 工具來做這件事。

我原本的做法是寫一支 Python 指令稿：用 `secrets` 產生隨機密碼、組出 `pikepdf.Permissions` 與 `pikepdf.Encryption` 物件，再用 `uv run --with pikepdf` 執行。能跑，但每次都要帶著那支指令稿，也要記得 `R=6`、`user=""` 這些參數的意義；想順便改 PDF 的標題或作者，還得再寫另一段程式。這種「每次都只想加個密碼」的小事，實在不該這麼複雜。

所以我把這個需求做成了一個獨立的命令列工具 **pdf-encrypt**。這篇文章就來介紹它的用法，並用同一個情境（免開啟密碼、但禁止修改）示範：過去需要一支指令稿的流程，現在一行指令就完成，順便把加解密、讀寫文件屬性的常用操作一併整理，讓你少走一點冤枉路。

![介紹好用工具：pdf-encrypt (針對 PDF 加解密與讀寫文件屬性)](https://stwillblogassets.blob.core.windows.net/files/images/2026/10/generate-write-protected-pdf-using-cli/generate-write-protected-pdf-using-cli-banner.webp)

> **測試環境宣告**：本篇文章實作於 macOS (Apple Silicon)，使用 `pdf-encrypt 0.2.0`，並以 poppler 的 `pdfinfo` 驗證結果。文中的檔名、文件屬性與密碼皆為示範，實作時請依自身環境與安全需求調整。

### pdf-encrypt 是什麼

pdf-encrypt 是我以 Rust 開發的命令列工具，可以加密、解密 PDF、設定檢視權限，並讀取與編輯文件屬性（Document Info 與 XMP）。它直接讀寫 PDF，**不依賴 qpdf、Ghostscript、pdftk 或 Python 環境**，裝好一個執行檔就能用，放進 CI 或批次指令稿也很單純。以 MIT 授權釋出。

它提供四個子命令：

-   `encrypt`：加密 PDF，可設定開啟密碼、擁有者密碼、加密演算法、檢視權限與文件屬性。
-   `decrypt`：解密並移除密碼與權限限制。
-   `info`：唯讀查詢文件屬性，可用 `--json` 輸出（別名 `properties`）。
-   `edit`：只修改指定的文件屬性，並保留既有加密設定。

### 安裝 pdf-encrypt

macOS 與 Linux 可以直接用安裝指令稿，預設會安裝到 `~/.local/bin`：

```bash
curl -fsSL https://raw.githubusercontent.com/doggy8088/pdf-encrypt/main/install.sh | sh
```

Windows 在 PowerShell 執行：

```powershell
irm https://raw.githubusercontent.com/doggy8088/pdf-encrypt/main/install.ps1 | iex
```

如果你已經有 Node.js 18 以上或 Rust 工具鏈，也可以改用套件管理員安裝：

```bash
# npm
npm install -g @willh/pdf-encrypt

# 或不安裝直接執行
npx @willh/pdf-encrypt --help

# Cargo
cargo install pdf-encrypt --locked
```

安裝完成後，用 `pdf-encrypt --version` 確認可以執行。完整的參數說明可以執行 `pdf-encrypt --help` 或 `pdf-encrypt encrypt --help` 查看。

### 釐清觀念：User Password 與 Owner Password 的本質差異

在動手之前，務必先弄清楚 PDF 規範（ISO 32000）定義的兩種密碼。很多人誤以為「密碼保護」只有一種密碼，結果設錯欄位，導致學員一拿到檔案就被要求輸入密碼：

| 密碼類型 | 控制範圍 | pdf-encrypt 參數 | 本篇需求的設定 |
| --- | --- | --- | --- |
| **開啟密碼**  <br>(User Password) | 是否允許「開啟並檢視」。設定後，閱讀器必須先提示輸入密碼。 | `-p`／`--open-password` | **不設定**（免密碼閱讀） |
| **權限密碼**  <br>(Owner Password) | 是否允許修改、註解、組合頁面等。輸入此密碼才能取得完整權限。 | `--owner-password` | **設定一組強密碼** |

pdf-encrypt 的密碼組合行為如下，這一點也是最容易設錯的地方：

-   只設 `-p`：開啟 PDF 需要這組密碼，而且擁有者密碼會自動設成相同值。
-   只設 `--owner-password`：**任何人都能直接開啟**，權限限制則由閱讀器套用。這正是本篇要的效果。
-   兩者都設：用開啟密碼進去時受權限限制，用擁有者密碼進去則享有完整權限。

因此，要達成「**學員免密碼開檔閱讀，但無法編輯內容**」，關鍵就是**只給 `--owner-password`，不要給 `-p`**。

### 實作步驟：一行指令加上防寫保護

以下假設你已經有一份轉好的 `slides.pdf`（如何從簡報轉出 PDF 不在本篇範圍）。

#### 步驟一：準備高強度的擁有者密碼

防寫密碼要有足夠的亂度。與其自己想一組，不如直接讓系統產生並放進環境變數，避免密碼留在 shell 歷史紀錄裡：

```bash
export PDF_OWNER_PASSWORD="$(openssl rand -base64 18)"
```

產生之後記得把這組密碼存進密碼管理員。之後若要重新編修或解鎖，都會用到它。

#### 步驟二：加密並禁止修改

接著只需要一行指令：

```bash
pdf-encrypt encrypt -i slides.pdf -o slides_protected.pdf \
  --owner-password "$PDF_OWNER_PASSWORD" \
  --deny modify --deny assemble --deny annotate
```

這幾個參數的意思如下：

-   `-i`、`-o`：輸入與輸出檔案。省略 `-o` 時，會另存為 `<原檔名>_encrypted.pdf`，不會動到原檔。
-   `--owner-password`：設定擁有者密碼；因為沒有給 `-p`，輸出的 PDF 不需要密碼就能開啟。
-   `--deny modify`：禁止修改內容。`--deny assemble`：禁止插入、刪除、旋轉頁面。`--deny annotate`：禁止新增或修改註解。
-   加密演算法預設就是 `aes-256`，不用另外指定。

實際跑起來會像這樣：

```
已完成加密：slides_protected.pdf
operation: encrypt
algorithm: aes-256
input_size: 9543
output_size: 11488
size_delta: 1945
input_encrypted: false
output_encrypted: true
pages: 1
objects: 12
```

如果只想要最常見的「唯讀」，可以用 `--read-only`（簡寫 `-ro`），它等於「允許全部權限，再禁止 `modify` 與 `assemble`」。這裡有個我實測才注意到的細節：**`--read-only` 並不會禁止註解**，如果也要禁止，必須再加上 `--deny annotate`：

```bash
pdf-encrypt encrypt -i slides.pdf -o slides_protected.pdf \
  --owner-password "$PDF_OWNER_PASSWORD" --read-only --deny annotate
```

可用的權限值有 `print`、`high-quality-print`、`modify`、`copy`、`annotate`、`fill-forms`、`accessibility`、`assemble` 與 `all`。`--allow` 是白名單模式（只允許列出的項目），`--deny` 則是從目前允許的項目中移除，評估順序是 `--allow`／`--read-only` 先，最後才套用 `--deny`。

#### 步驟三：一併寫入文件屬性

釋出給學員的檔案，標題、作者與著作權聲明最好也一起補齊。`encrypt` 可以在加密的同時寫入文件屬性，不需要另外跑一次：

```bash
pdf-encrypt encrypt -i slides.pdf -o slides_protected.pdf \
  --owner-password "$PDF_OWNER_PASSWORD" \
  --deny modify --deny assemble --deny annotate \
  --doc-title "企業內訓講義" \
  --doc-author "Will 保哥" \
  --doc-keywords "PDF, 內訓" \
  --doc-copyright-notice "Copyright (c) Will 保哥"
```

目前支援八個欄位：`--doc-title`、`--doc-author`、`--doc-subject`、`--doc-keywords`、`--doc-description`、`--doc-copyright-status`、`--doc-copyright-notice`、`--doc-copyright-info-url`。這些值會同時寫入 Document Info 與 XMP。

#### 步驟四：注意覆蓋確認提示

當輸出檔已經存在，或使用 `--in-place` 直接覆蓋原檔時，pdf-encrypt 會在終端機詢問 `[y/N]`。在 CI 或批次指令稿這類沒有人可以輸入的環境，我實測會直接取消並以非零代碼結束：

```
即將覆蓋檔案：slides_protected.pdf，是否繼續？[y/N] 處理失敗：已取消覆蓋：slides_protected.pdf
```

確定要覆蓋時，加上 `--force`（`-f`）即可略過確認。預設保留原檔是刻意的設計，建議不要輕易改用 `--in-place`。

### 雙向驗證：確認防寫保護真的生效

安全性設定最忌諱「自己以為設定成功，實際上沒有防護到」，所以產出後要做正向與反向的驗證。

#### 1\. 正向驗證：免密碼開檔

用系統預設的 PDF 閱讀器（macOS 預覽程式、Chrome 或 Adobe Acrobat Reader）點開產出的檔案，**應該立即開啟，完全不會出現密碼提示**。

#### 2\. 反向驗證：檢查加密與權限中繼資料

接著用 `pdfinfo` 檢查 PDF 內部的安全性資訊：

```bash
pdfinfo slides_protected.pdf
```

節錄輸出如下：

```
Title:           企業內訓講義
Keywords:        PDF, 內訓
Author:          Will 保哥
Encrypted:       yes (print:yes copy:yes change:no addNotes:no algorithm:AES-256)
PDF version:     2.0
```

關鍵數值一目了然：

-   `Encrypted: yes` 搭配 `algorithm:AES-256`：使用了高強度的現代加密演算法。
-   `change:no` 與 `addNotes:no`：文件已被鎖定，禁止修改與新增註解。
-   `print:yes copy:yes`：仍可列印與複製文字，符合「公開閱讀」的需求。

如果漏掉 `--deny annotate`，這裡會看到 `addNotes:yes`，這也是我建議一定要驗證一次的原因。

此外，pdf-encrypt 本身也能用 `info` 讀回文件屬性，不必依賴其他工具：

```bash
pdf-encrypt info -i slides_protected.pdf --json
```

```json
{
  "conflicts": [],
  "document_info": {
    "author": "Will 保哥",
    "copyright_notice": "Copyright (c) Will 保哥",
    "keywords": "PDF, 內訓",
    "title": "企業內訓講義"
  },
  "input_encrypted": true,
  "pages": 1,
  "pdf_version": "2.0"
}
```

上面是節錄；實際輸出還包含 `info_dictionary`、`xmp_properties` 與 `warnings` 等欄位。JSON 中的 `null` 代表該欄位沒有值，Document Info 與 XMP 互相不一致時會列在 `conflicts`，很適合拿來寫自動化檢查。

### 日常維護：修改文件屬性與解密

檔案釋出後，常常還會需要改標題或補上主旨。用 `edit` 只改你指定的欄位，其他屬性與既有的加密設定都會保留：

```bash
pdf-encrypt edit -i slides_protected.pdf -o slides_edited.pdf --doc-subject "Day 1"
```

```
已完成文件屬性編輯：slides_edited.pdf
operation: edit
input_encrypted: true
output_encrypted: true
```

實測後再用 `pdfinfo` 檢查，`Title` 維持原值、`Subject` 變成 `Day 1`，而且仍然是 `Encrypted: yes (... algorithm:AES-256)`。傳入空字串（例如 `--doc-keywords ""`）則會清空該欄位。

需要完全移除保護時，使用 `decrypt`：

```bash
pdf-encrypt decrypt -i slides_protected.pdf -p "$PDF_OWNER_PASSWORD"
```

```
已完成解密：slides_protected_decrypted.pdf
operation: decrypt
input_encrypted: true
output_encrypted: false
```

另外，如果輸入檔本來就已加密，想換密碼或換演算法（例如為了相容舊版閱讀器改用 `aes-128`），可以用 `--input-open-password` 帶入舊密碼，一次重新加密：

```bash
pdf-encrypt encrypt -i old.pdf -o new.pdf --algorithm aes-128 \
  --input-open-password "$OLD_PASSWORD" --owner-password "$PDF_OWNER_PASSWORD" --read-only
```

### 一個必須知道的限制：權限不是存取控制

這是這次實測中最值得記錄的地方。我故意對「免開啟密碼」的 PDF 執行 `decrypt` 並帶入一組**錯誤**的密碼，結果它一樣成功解密：

```bash
pdf-encrypt decrypt -i slides_protected.pdf -o open.pdf -p 'wrong'
```

原因是這份 PDF 的開啟密碼是空字串，工具可以直接用空密碼開檔，再移除權限限制。這並不是工具的漏洞，而是 PDF 權限機制的本質：**權限限制只是要求閱讀器「應該遵守」的規範，並不是真正的存取控制**。因此本篇的做法適合「防止一般人輕易編修、避免誤改」，如果文件內容本身需要保密，必須設定夠長的**開啟密碼**（`-p`），並維持預設的 `aes-256`。

### 總結

用 pdf-encrypt 之後，原本需要 Python 指令稿才能完成的流程，變成了這樣：

1.  **觀念對應到參數**：只給 `--owner-password` 就是免密碼閱讀加防寫，不必再記 `user=""`、`R=6` 這類底層細節。
2.  **單一執行檔**：不用 `uv`、不用虛擬環境、不用 qpdf，安裝一次就能在本機與 CI 重複使用。
3.  **加密與文件屬性一次處理**：`encrypt`、`edit`、`info`、`decrypt` 四個子命令涵蓋日常 PDF 保護與維護需求。

如果你平時也有將講義、商業報告或報價單轉為受保護 PDF 的需求，歡迎試試這個工具。使用上遇到問題或有功能建議，也可以直接到 GitHub 開 Issue。

### 相關連結

-   [pdf-encrypt 官方網站](https://pdf-encrypt.gh.miniasp.com/)
-   [doggy8088/pdf-encrypt - GitHub Repo](https://github.com/doggy8088/pdf-encrypt)
-   [pikepdf Documentation - Security and Encryption](https://pikepdf.readthedocs.io/)
-   [Adobe PDF Reference - Document Security](https://opensource.adobe.com/dc-acrobat-sdk-docs/)
