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

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

mstamy2/PyPDF2: A utility to read and write PDFs with Python

既存のPDFファイルにパスワードを設定して保護したり、パスワード付きの暗号化されたPDFファイルをパスワードなしのPDFファイルとして保存したりすることができます。 あくまでも既知のパスワードを使って解除するだけで、パスワードが分からない場合は解除できない(総当たりでパスワードを試すスクリプトを作成することはできる)。また、暗号化アルゴリズムはRC4のみに対応しAESに対応していないため、最近のソフトで暗号化されたファイルは解除できない場合が多い(バージョン1.26.0時点)。 ここでは以下の内容について説明します。

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

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

python-snippets/notebook/data/src/pdf

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

PyPDF2のインストール

PyPDF2は外部ライブラリに依存していません。pip(pip3)やcondaでインストールできます。 $ pip install PyPDF2

以下のサンプルコードで使用しているPyPDF2のバージョンは1.26.0。 クラスやメソッドなどの詳細は公式ドキュメントを参照。

PyPDF2 Documentation — PyPDF2 1.26.0 documentation

IssueやPull Requestが溜まっており活発に開発されているという状況ではないが、シンプルなPDFファイルの処理であれば問題ない。

mstamy2/PyPDF2: A utility to read and write PDFs with Python

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

暗号化アルゴリズムによってはパスワードを解除できない 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

ソースコードからも分かるように、PyPDF2(バージョン1.26.0時点)で対応している暗号化アルゴリズムはRC4のみ。AESで暗号化されたファイルは復号(パスワード解除)できません。

PyPDF2/pdf.py at 1.26.0 · mstamy2/PyPDF2 PyPDF2/utils.py at 1.26.0 · mstamy2/PyPDF2

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

qpdf/qpdf: Primary QPDF source code and documentation

パスワードを設定し保存する際に時間がかかる場合がある サイズが大きいファイルにパスワードを設定して保存すると処理時間が長くなる場合があります。例えば、500ページ / 10MB程度の電子書籍にパスワードを設定して保存すると1分強かかった。当然、マシンパワーにも依存するだろうが、大きいサイズのファイルを一括で処理したい場合などは気をつけてください。 情報が失われる可能性がある これはPyPDF2の制約というよりも以下で説明するサンプルコードについての注意点。 以下のサンプルコードではいくつかのファイルで試して画像やレイアウト、メタデータ(作成者やタイトルなど)が保持されていることを確認したが、複雑なセキュリティ設定やAcrobat等の最新機能を駆使した場合にそれらの情報がそのまま保存されるかは未確認。 元のPDFファイルは残しておくことを強く推奨します。

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

PyPDF2のPdfFileReaderクラスのisEncrypted属性で、PDFファイルが暗号化されているかどうかを確認できます。 PdfFileReaderはコンストラクタにPDFファイルのパスを指定して生成します。isEncryptedという名前の通り、暗号されていればTrue、いなければFalseとなります。

import PyPDF2

pdf = PyPDF2.PdfFileReader('data/src/pdf/sample1.pdf')

print(pdf.isEncrypted)
# False

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

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

元のPDFファイルからPdfFileReaderオブジェクトを生成 空のPdfFileWriterオブジェクトを作成 PdfFileReaderオブジェクトの中身をコピー PdfFileWriterオブジェクトにパスワードを設定 PdfFileWriterオブジェクトをPDFファイルとして保存

PdfFileReaderとPdfFileWriterをそれぞれのコンストラクタから生成。 src_pdf = PyPDF2.PdfFileReader('data/src/pdf/sample1.pdf')

dst_pdf = PyPDF2.PdfFileWriter()

cloneReaderDocumentRoot()でドキュメントの内容をコピー。 dst_pdf.cloneReaderDocumentRoot(src_pdf)

作成者やタイトルなどのメタデータもそのままでパスワードを付与したい場合は、メタデータもコピーします。必要なければ省略して問題ありません。 メタデータはPdfFileReaderオブジェクトのdocumentInfo属性で取得し、PdfFileWriterオブジェクトのaddMetadata()メソッドで追加できます。ファイルによってはdocumentInfo属性で取得した辞書をそのままaddMetadata()メソッドの引数にするとエラーになる(ならないファイルもある)。

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

# dst_pdf.addMetadata(src_pdf.documentInfo)
# TypeError: createStringObject should have str or unicode arg

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

d = {key: src_pdf.documentInfo[key] for key in src_pdf.documentInfo.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.addMetadata(d)

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

encrypt()メソッドで暗号化。引数にパスワードを指定します。第一引数user_pwdでユーザーパスワード、第二引数owner_pwdでオーナーパスワードを設定できます。第二引数を省略するとオーナーパスワードも第一引数に指定した文字列となります。

The PdfFileWriter Class: encrypt() — PyPDF2 1.26.0 documentation

dst_pdf.encrypt('password')

最後にwrite()メソッドを使用してPdfFileWriterオブジェクトをpdfファイルとして保存します。 write()の引数はパス文字列ではなくファイルオブジェクトである必要があるのでopen()を使います。wbで書き込み用のバイナリファイルとしてオープンします。

with open('data/temp/sample1_pass.pdf', 'wb') as f:
    dst_pdf.write(f)

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

def set_password(src_path, dst_path, password):
    src_pdf = PyPDF2.PdfFileReader(src_path)

    dst_pdf = PyPDF2.PdfFileWriter()
    dst_pdf.cloneReaderDocumentRoot(src_pdf)

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

    dst_pdf.encrypt(password)

    with open(dst_path, 'wb') as f:
        dst_pdf.write(f)

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

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

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

パスワード付きのPDFファイルのパスワードを削除したり変更したりして保存するのも、上述のパスワードの設定とほぼ同じ流れになります。

元のPDFファイルからPdfFileReaderオブジェクトを生成 PdfFileReaderオブジェクトのパスワードを解除 PdfFileWriterオブジェクトを作成 PdfFileReaderオブジェクトの中身をコピー PdfFileWriterオブジェクトにパスワードを設定 PdfFileWriterオブジェクトをPDFファイルとして保存

パスワードを解除するステップが増えただけです。 暗号化されたPDFファイルから生成したPdfFileReaderオブジェクトではdocumentInfo属性などの情報にアクセスできません。

pdf_rc4 = PyPDF2.PdfFileReader('data/src/pdf/sample1_pass_rc4.pdf')

print(pdf_rc4.isEncrypted)
# True

# print(pdf_rc4.documentInfo)
# PdfReadError: file has not been decrypted

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

The PdfFileReader Class: decrypt() — PyPDF2 1.26.0 documentation

解除後はdocumentInfo属性などの情報にアクセスできます。

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

print(pdf_rc4.decrypt('password'))
# 1

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

PyPDF2が対応していない暗号化アルゴリズムに対してはNotImplementedError例外が送出されます。上述のようにPyPDF2はAESに対応していない(バージョン1.26.0時点)ので、該当のファイルのパスワードは解除できません。

pdf_aes = PyPDF2.PdfFileReader('data/src/pdf/sample1_pass_aes.pdf')

# print(pdf_aes.decrypt('password'))
# NotImplementedError: only algorithm code 1 and 2 are supported

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

def change_password(src_path, dst_path, src_password, dst_password=None):
    src_pdf = PyPDF2.PdfFileReader(src_path)
    src_pdf.decrypt(src_password)

    dst_pdf = PyPDF2.PdfFileWriter()
    dst_pdf.cloneReaderDocumentRoot(src_pdf)

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

    if dst_password:
        dst_pdf.encrypt(dst_password)

    with open(dst_path, 'wb') as f:
        dst_pdf.write(f)

パスワードを削除したい場合は引数dst_passwordを省略すると出力ファイルは暗号化されず、パスワードなしのPDFファイルとなります。パスワードを変更したい場合は引数dst_passwordに新たなパスワードを指定すれば問題ありません。

change_password('data/src/pdf/sample1_pass_rc4.pdf', 'data/temp/sample1_no_pass.pdf', 'password')

change_password('data/src/pdf/sample1_pass_rc4.pdf', 'data/temp/sample1_new_pass.pdf',
                'password', 'new_password')
Last Updated: 6/26/2019, 10:34:03 PM