Python, PyPDF2でPDFのパスワードを設定・解除(暗号化・復号)
PythonのサードパーティライブラリPyPDF2を使うと、PDFファイルのパスワードの設定や解除(暗号化・復号)ができる。
既存のPDFファイルにパスワードを設定して保護したり、パスワード付きの暗号化されたPDFファイルをパスワードなしのPDFファイルとして保存したりすることが可能。
あくまでも既知のパスワードを使って解除するだけで、パスワードが分からない場合は解除できない(総当たりでパスワードを試すスクリプトを作成することはできる)。また、暗号化アルゴリズムはRC4のみに対応しAESに対応していないため、最近のソフトで暗号化されたファイルは解除できない場合が多い(バージョン1.26.0
時点)。
ここでは以下の内容について説明する。
- PyPDF2のインストール
- PyPDF2によるパスワード処理の制約
- PDFファイルが暗号化されているか確認
- PDFファイルにパスワードを設定して保存
- PDFファイルのパスワードを解除(削除・変更)して保存
サンプルで使用しているPDFファイルは以下のリンクから。暗号化されているファイルのパスワードはすべてpassword
。
すべてのPDFファイルに対して動作を保証するものではない。
PyPDF2のインストール
PyPDF2は外部ライブラリに依存していない。pip
(pip3
)やconda
でインストール可能。
$ pip install PyPDF2
以下のサンプルコードで使用しているPyPDF2のバージョンは1.26.0
。
クラスやメソッドなどの詳細は公式ドキュメントを参照。
IssueやPull Requestが溜まっており活発に開発されているという状況ではないが、シンプルなPDFファイルの処理であれば問題ない。
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で暗号化されたファイルは復号(パスワード解除)できない。
Pythonのライブラリではないが、コマンドラインでPDFファイルのセキュリティ設定を制御できるものとしてqpdfがある。AESにも対応している。
パスワードを設定し保存する際に時間がかかる場合がある
サイズが大きいファイルにパスワードを設定して保存すると処理時間が長くなる場合がある。例えば、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)
作成者やタイトルなどのメタデータもそのままでパスワードを付与したい場合は、メタデータもコピーする。必要なければ省略してOK。
メタデータは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
でオーナーパスワードを設定可能。第二引数を省略するとオーナーパスワードも第一引数に指定した文字列となる。
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
を返す。
解除後は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
に新たなパスワードを指定すればOK。
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')