介紹好用工具: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 加解密與讀寫文件屬性)

測試環境宣告:本篇文章實作於 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:

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

Windows 在 PowerShell 執行:

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

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

# 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 參數 本篇需求的設定
開啟密碼
(User Password)
是否允許「開啟並檢視」。設定後,閱讀器必須先提示輸入密碼。 -p/--open-password 不設定(免密碼閱讀)
權限密碼
(Owner Password)
是否允許修改、註解、組合頁面等。輸入此密碼才能取得完整權限。 --owner-password 設定一組強密碼

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

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

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

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

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

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

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

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

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

步驟二:加密並禁止修改

接著只需要一行指令:

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:

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 可以在加密的同時寫入文件屬性,不需要另外跑一次:

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 內部的安全性資訊:

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 讀回文件屬性,不必依賴其他工具:

pdf-encrypt info -i slides_protected.pdf --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 只改你指定的欄位,其他屬性與既有的加密設定都會保留:

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:

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 帶入舊密碼,一次重新加密:

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 並帶入一組錯誤的密碼,結果它一樣成功解密:

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。

相關連結

留言評論