「このPDF、パスワードをかけて渡したい」「もらったPDFのパスワードを外したい」——地味だけど、意外とよくある作業です。でも無料の変換サイトはPDFをアップロードさせるものが多く、契約書や請求書のような中身を見られたくないファイルだと、正直ちょっと怖いです。
この記事では、PDFをアップロードせず、ブラウザの中だけでパスワードをかける/解除する方法を qpdf の WebAssembly 版でまとめます。おまけに Python(pikepdf)でのやり方も載せます。
ゴール:サーバーに送らず、PDFに AES-256 のパスワードをかけたり、正しいパスワードで解除したりできるようになること。
qpdf を WebAssembly でブラウザに持ち込む
qpdf は PDF の暗号化・解除を昔からしっかりやってくれる定番ツールです。これを wasm にした @jspawn/qpdf-wasm を使うと、qpdf のコマンドをそのままブラウザ内で実行できます。
<script src="https://cdn.jsdelivr.net/npm/@jspawn/qpdf-wasm@0.0.2/qpdf.js"></script>
Emscripten 製なので、グローバルの Module ファクトリを呼び出してインスタンスを作り、仮想ファイルシステム(FS)にPDFを書き込んで callMain でCLI引数を渡す、という流れになります。1回の操作ごとに新しいインスタンスを作るのが安全です。
// args: qpdfに渡すCLI引数(最後の要素が出力ファイル名)
// inputBytes: 入力PDF(Uint8Array)
async function runQpdf(args, inputBytes) {
const mod = await Module({
noInitialRun: true,
locateFile: (p) => `https://cdn.jsdelivr.net/npm/@jspawn/qpdf-wasm@0.0.2/${p}`,
});
mod.FS.writeFile("in.pdf", inputBytes);
let rc;
try {
rc = mod.callMain(args); // 成功で 0
} catch (e) {
rc = e && e.status != null ? e.status : 1;
}
const out = rc === 0 ? mod.FS.readFile(args[args.length - 1]) : null;
return { rc, out }; // out は Uint8Array
}
ファイル選択はいつもの <input type="file"> から arrayBuffer() で読み込みます。
const bytes = new Uint8Array(await file.arrayBuffer());
パスワードをかける(AES-256で暗号化)
暗号化は qpdf の --encrypt を使います。書式は --encrypt <ユーザーPW> <オーナーPW> <鍵長> -- 入力 出力 です。
async function encryptPdf(bytes, pw) {
const { rc, out } = await runQpdf(
["--encrypt", pw, pw, "256", "--", "in.pdf", "out.pdf"],
bytes
);
if (rc !== 0 || !out) throw new Error("暗号化に失敗しました");
return out; // 暗号化済みPDF(Uint8Array)
}
- ユーザーPW:PDFを開くときに聞かれるパスワード
- オーナーPW:印刷・コピーなどの権限用パスワード(今回は同じにしています)
-
256:AES-256。128も指定できます -
--:ここから先はオプションではなく入力ファイル名、という区切り
保存はBlobにしてダウンロードすればOKです。
function download(bytes, name) {
const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
const a = Object.assign(document.createElement("a"), { href: url, download: name });
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
パスワードを解除する(正しいパスワードが必要)
解除は --decrypt と --password= です。
async function decryptPdf(bytes, pw) {
const { rc, out } = await runQpdf(
["--decrypt", "--password=" + pw, "in.pdf", "out.pdf"],
bytes
);
if (rc !== 0 || !out) {
// パスワードが違う、または対応していないPDF
throw new Error("解除できませんでした(パスワードが違う可能性があります)");
}
return out; // パスワードなしで開けるPDF
}
ここが地味に便利なポイントで、パスワードが違うと callMain の戻り値(rc)が 0 以外になります(qpdf は 2 を返します)。例外を投げるわけではないので、rc を見て「パスワードが違います」と優しく出してあげられます。
もちろん、総当たりでパスワードを破ることはしません。あくまで「正しいパスワードを知っている人が外す」ためのものです。
Python でやる場合(pikepdf)
サーバー側やバッチ処理でやりたいときは、pikepdf(中身はやはり qpdf)が手軽です。
import pikepdf
# パスワードをかける(R=6 が AES-256)
pdf = pikepdf.open("input.pdf")
pdf.save(
"protected.pdf",
encryption=pikepdf.Encryption(owner="pw", user="pw", R=6),
)
# パスワードを解除する(正しいパスワードが必要)
pdf = pikepdf.open("protected.pdf", password="pw")
pdf.save("unlocked.pdf")
パスワードが違うと pikepdf.PasswordError が飛ぶので、そこで判定できます。
つまずきやすいところ
- パスワードを忘れると解除できません。 qpdf も pikepdf も「正しいパスワードで外す」ためのもので、破るためのものではありません。大切に保管を。
-
すでに暗号化されたPDFに、さらに
--encryptはできません。 いったん--decrypt(現在のパスワードが必要)してから、かけ直します。 -
qpdf-wasm はシングルスレッドなので、
SharedArrayBufferは不要=COOP/COEP のヘッダーは要りません。ffmpeg.wasm のマルチスレッド版のような設定は不要で、導入がかなり楽です。 -
CSP を使っているなら
'wasm-unsafe-eval'をscript-srcに許可しておきます(wasm のコンパイルに必要)。 - 動作確認は正しいPDFで。手書きで作った不正なPDF(xrefがズレているなど)だと qpdf が
Unsupported data typeなどで弾くので、テストは実在のPDFやライブラリ生成のPDFで行うのが確実です。
コードを書かずに使いたいとき
「1回だけかけたい/外したい」「PDFをアップロードしたくない」なら、ブラウザ完結のツールが手軽です。
私が作っている Morphy の「PDFパスワード」なら、
- PDFを選んで、かける/解除するを切り替えるだけ
- 暗号化は AES-256、解除は現在のパスワードを入れるだけ
- 処理はすべてブラウザ内で完結し、PDFもパスワードもアップロードされません
- 登録不要・無料
👉 PDFパスワード(設定・解除)ツール:https://morphytools.com/pdf-password.html
まとめ
-
qpdf-wasmを使えば、サーバーなし・ブラウザだけでPDFにパスワードをかけたり外したりできる - かける=
--encrypt user owner 256 -- in out、外す=--decrypt --password=... in out - パスワード違いは rc が 0 以外で判定できる(例外は飛ばない)
- Python なら pikepdf が手軽(
R=6で AES-256) - シングルスレッドなので COOP/COEP 不要、CSP は
'wasm-unsafe-eval'だけ気にすればよい
「中身を見られたくないPDFを、アップロードせずにパスワード管理したい」に、ブラウザ完結はよく合います。参考になれば嬉しいです。