Python, pypdfでPDFのパスワードを設定・解除(暗号化・復号)

Modified: | Tags: Python, PDF

Pythonのサードパーティライブラリpypdf(旧PyPDF2)を使うと、PDFファイルのパスワードの設定や解除(暗号化・復号)ができる。

既存のPDFファイルにパスワードを設定して保護したり、パスワード付きの暗号化されたPDFファイルをパスワードなしのPDFファイルとして保存したりすることが可能。あくまでも既知のパスワードを使って解除するだけで、パスワードが分からない場合は解除できない。

ここでは以下の内容について説明する。

  • pypdfのインストール
  • pypdfによるパスワード処理の制約
  • PDFファイルが暗号化されているか確認
  • PDFファイルにパスワードを設定して保存
  • PDFファイルのパスワードを解除(削除・変更)して保存

サンプルで使用しているPDFファイルは以下のリンクから。暗号化されているファイルのパスワードはすべてpassword

すべてのPDFファイルに対して動作を保証するものではない。

pypdfのインストール

pypdfは外部ライブラリに依存しておらず、pippip3)でインストール可能。AES方式を復号を利用する場合は下のようにpypdf[crypto]とする。

$ pip install pypdf
$ pip install pypdf[crypto]

以下のサンプルコードで使用しているpypdfのバージョンは3.7.1

以前はPyPDF2という名前だったが、2023年にpypdfに改められた。

pypdfによるパスワード処理の制約

AESでの暗号化はできない

Adobe AcrobatでPDFファイルにパスワードを設定する場合、以下の暗号化アルゴリズム(暗号化レベル)を選択できる。

「Acrobat 6 > .0 およびそれ以降」(PDF 1.5)を選択すると、128-bit RC4 を使用して文書が暗号化されます。
「Acrobat 7.0 およびそれ以降」(PDF 1.6)を選択すると、128-bit キーサイズの AES 暗号化アルゴリズムを使用して文書が暗号化されます。
「Acrobat X およびそれ以降」(PDF 1.7)を選択すると、256-bit AES を使用して文書が暗号化されます。Acrobat 8 および 9 で作成した文書に 256-bit AES 暗号化を適用するには、「Acrobat X およびそれ以降」を選択します。
パスワードによる PDF の保護 - Adobe Acrobat

pypdf(3.7.1時点)ではAESでの暗号化はできない(復号はできる)。以下の警告にもあるようにRC4での暗号化は安全でないので注意。

WARNING: pypdf only implements RC4 encryption. This encryption algorithm is insecure. The more modern and secure AES encryption is not implemented. pypdf can only decrypt, but not encrypt with AES. Encryption and Decryption of PDFs — pypdf 3.7.1 documentation

Pythonのライブラリではないが、コマンドラインでPDFファイルのセキュリティ設定を制御できるものとしてqpdfがある。AESにも対応している。

情報が失われる可能性がある

これはpypdfの制約というよりも以下で説明するサンプルコードについての注意点。

以下のサンプルコードではいくつかのファイルで試して画像やレイアウト、メタデータ(作成者やタイトルなど)が保持されていることを確認したが、複雑なセキュリティ設定やAcrobat等の最新機能を駆使した場合にそれらの情報がそのまま保存されるかは未確認。

元のPDFファイルは残しておくことを強く推奨する。

PDFファイルが暗号化されているか確認

PdfReaderオブジェクトのis_encrypted:属性で、PDFファイルが暗号化されているかどうかを確認できる。

import pypdf

print(pypdf.__version__)
# 3.7.1

pdf = pypdf.PdfReader('data/src/pdf/sample1.pdf')
print(pdf.is_encrypted)
# False

PDFファイルにパスワードを設定して保存

以下の流れでパスワードなしのPDFファイルにパスワードを設定して保存する。

  1. 元のPDFファイルからPdfReaderオブジェクトを生成
  2. 空のPdfWriterオブジェクトを作成
  3. PdfReaderオブジェクトの中身をコピー
  4. PdfWriterオブジェクトにパスワードを設定
  5. PdfWriterオブジェクトをPDFファイルとして保存

PdfReaderPdfWriterをそれぞれ生成し、clone_reader_document_root()でドキュメントの内容をコピー。

src_pdf = pypdf.PdfReader('data/src/pdf/sample1.pdf')
dst_pdf = pypdf.PdfWriter()
dst_pdf.clone_reader_document_root(src_pdf)

作成者やタイトルなどのメタデータもそのままでパスワードを付与したい場合は、メタデータもコピーする。必要なければ省略してよい。

メタデータはPdfReaderオブジェクトのmetadata属性で取得し、PdfWriterオブジェクトのadd_metadata()メソッドで追加できる。ファイルによってはmetadata属性で取得した辞書をそのままadd_metadata()メソッドの引数にするとエラーになる(ならないファイルもある)。

print(src_pdf.metadata)
# {'/Title': IndirectObject(33, 0, 4435217808), '/Producer': IndirectObject(34, 0, 4435217808), '/Creator': IndirectObject(35, 0, 4435217808), '/CreationDate': IndirectObject(36, 0, 4435217808), '/ModDate': IndirectObject(36, 0, 4435217808)}

# dst_pdf.add_metadata(src_pdf.metadata)
# TypeError: create_string_object should have str or unicode arg

もっといい方法があるかもしれないが、ここでは新たな辞書を生成してからadd_metadata()メソッドに渡す。辞書内包表記を使う。

d = {key: src_pdf.metadata[key] for key in src_pdf.metadata.keys()}
print(d)
# {'/Title': 'sample1', '/Producer': 'macOS バージョン10.14.2(ビルド18C54) Quartz PDFContext', '/Creator': 'Keynote', '/CreationDate': "D:20190114072947Z00'00'", '/ModDate': "D:20190114072947Z00'00'"}

dst_pdf.add_metadata(d)

メタデータについては以下の記事も参照。

encrypt()メソッドで暗号化。引数にパスワードを指定する。第一引数user_pwdでユーザーパスワード、第二引数owner_pwdでオーナーパスワード(権限パスワード)を設定可能。第二引数を省略(デフォルト値はNone)するとオーナーパスワードも第一引数に指定した文字列となる。

最後にwrite()メソッドでPdfWriterオブジェクトをpdfファイルとして保存する。

dst_pdf.encrypt('pass_u', 'pass_o')
dst_pdf.write('data/temp/sample1_pass.pdf')

まとめて関数化すると以下のようになる。例外処理は省略。

def set_password(src_path, dst_path, user_password, owner_password=None):
    src_pdf = pypdf.PdfReader(src_path)
    dst_pdf = pypdf.PdfWriter()
    dst_pdf.clone_reader_document_root(src_pdf)

    d = {key: src_pdf.metadata[key] for key in src_pdf.metadata.keys()}
    dst_pdf.add_metadata(d)

    dst_pdf.encrypt(user_password, owner_password)
    dst_pdf.write(dst_path)

set_password('data/src/pdf/sample1.pdf', 'data/temp/sample1_pass.pdf',
             'pass_u', 'pass_o')

出力パスを入力パス(元ファイルのパス)と同じにすると元のPDFファイルが上書きされるが、上述のように、複雑なPDFファイルなどはすべての情報が保持されるか未確認なので、元ファイルは上書きせずに残しておくことを推奨する。

PDFファイルのパスワードを解除(削除・変更)して保存

パスワード付きのPDFファイルのパスワードを削除したり変更したりして保存するのも、上述のパスワードの設定とほぼ同じ流れ。パスワードを解除するステップが増えるだけ。

  1. 元のPDFファイルからPdfReaderオブジェクトを生成
  2. PdfReaderオブジェクトのパスワードを解除
  3. PdfWriterオブジェクトを作成
  4. PdfReaderオブジェクトの中身をコピー
  5. PdfWriterオブジェクトにパスワードを設定
  6. PdfWriterオブジェクトをPDFファイルとして保存

暗号化されたPDFファイルから生成したPdfReaderオブジェクトではmetadata属性などの情報にアクセスできない。

pdf_pass = pypdf.PdfReader('data/temp/sample1_pass.pdf')
print(pdf_pass.is_encrypted)
# True

# print(pdf_pass.metadata)
# FileNotDecryptedError: File has not been decrypted

パスワードの解除にはdecrypt()メソッドを使う。パスワードが一致しない場合は0、ユーザーパスワードに一致した場合は1、オーナーパスワードに一致した場合は2を返す。

解除後はmetadata属性などの情報にアクセスできる。

print(pdf_pass.decrypt('wrong-password'))
# 0

print(pdf_pass.decrypt('pass_u'))
# 1

print(pdf_pass.decrypt('pass_o'))
# 2

print(pdf_pass.metadata)
# {'/Producer': 'macOS バージョン10.14.2(ビルド18C54) Quartz PDFContext', '/Title': 'sample1', '/Creator': 'Keynote', '/CreationDate': "D:20190114072947Z00'00'", '/ModDate': "D:20190114072947Z00'00'"}

パスワードを解除(削除・変更)して保存する処理を関数化すると以下のようになる。

def change_password(
    src_path, dst_path, src_password, dst_user_password=None, dst_owner_password=None
):
    src_pdf = pypdf.PdfReader(src_path)
    src_pdf.decrypt(src_password)

    dst_pdf = pypdf.PdfWriter()
    dst_pdf.clone_reader_document_root(src_pdf)

    d = {key: src_pdf.metadata[key] for key in src_pdf.metadata.keys()}
    dst_pdf.add_metadata(d)

    if dst_user_password is not None:
        dst_pdf.encrypt(dst_user_password, dst_owner_password)

    dst_pdf.write(dst_path)

パスワードを削除したい場合は引数dst_user_passwordを省略すると出力ファイルは暗号化されず、パスワードなしのPDFファイルとなる。パスワードを変更したい場合は引数dst_user_passwordおよびdst_owner_passwordに新たなパスワードを指定すればよい。dst_owner_passwordを省略するとオーナーパスワードはユーザーパスワードと同じになる。

change_password('data/temp/sample1_pass.pdf', 'data/temp/sample1_no_pass.pdf',
                'pass_u')

change_password('data/temp/sample1_pass.pdf', 'data/temp/sample1_new_pass.pdf',
                'pass_u', 'new_pass_u', 'new_pass_o')

関連カテゴリー

関連記事