0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PDFにパスワードをかける・解除する方法まとめ(JS qpdf-wasm でブラウザ完結/Python pikepdf)

0
Posted at

「この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を、アップロードせずにパスワード管理したい」に、ブラウザ完結はよく合います。参考になれば嬉しいです。


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?