AES対応のPython暗号化ライブラリを比較検証してみた

こんにちは。DA事業本部の春田です。

PythonでAES方式の暗号化を実現したかったため、暗号化系のライブラリが複数あったので比較検証してみました。

さっそくですが、「Python 暗号化」でググるとよくヒットするpycryptoというライブラリは、今回対象外としました。pycryptoライブラリはバグの報告が上がっているものの2013年10月以降アップデートが止まっているので、そもそもの使用を控えることにします。

今回比較するのは、PyCryptodomepyca/cryptographyです。

PyCryptodome

PyCryptodomeはLow-levelな暗号化機能が使えるPythonライブラリです。Python 2.6, 2.7, 3.4+に対応しています。先のPyCryptoからフォークしたライブラリで、後方互換性もある程度意識されているみたいですが、主要クラスのメソッドで削除されたものもあります。(詳しくはこちら→Compatibility with PyCrypto — PyCryptodome 3.9.7 documentation

共通鍵暗号から、公開鍵暗号、認証付き暗号、暗号学的ハッシュなど、非常に多くの方式に対応しています。各種サブパッケージ をimportし、インスタンスとメソッドで実装を進めていく形となります。にしても、ドキュメントが整っていてかなり見やすい。

軽くAESを試してみました。インストールはおなじみのpipから可能です。

pip install pycryptodome

AESと乱数生成用のパッケージをimportします。

>>> from Crypto.Cipher import AES
>>> from Crypto.Random import get_random_bytes
>>> key = get_random_bytes(16)
>>> key
b"\x0f0oj'\xfc\x14h\xf0L\x99\xd9\xaf=\xdd\xfb"
>>> cipher = AES.new(key, AES.MODE_EAX)

暗号化したいTarget dataという文字列を引数に、encrypt_and_digestメソッドを使用すると、暗号化済みのテキストとMAC tagが返されます。

>>> ciphertext, tag = cipher.encrypt_and_digest(b'Target data')
>>> ciphertext
b'\xf8lf\xfa\xc2P#\x96\x12\x91\xc1'
>>> tag
b'\x889\xc9\xf0\xcd\xb9j-\xe1\xab\xbf\x0cF?L\xa1'
>>> cipher.nonce
b'~\x07\xc8l\xb6p}MWa\x9e{\xb4\x13"\x14'

復号する時はナンスを固定してインスタンスを作成し、decrypt_and_verifyメソッドに暗号化済みのテキストとtagを渡せばOKです。

>>> cipher_dec = AES.new(key, AES.MODE_EAX, cipher.nonce)
>>> data = cipher_dec.decrypt_and_verify(ciphertext, tag)
>>> data
b'Target data'

Low-levelとの記載がありましたが、この時点でもかなりお手軽に使えました。

pyca/cryptography

一方、pyca/cryptographyの方は、Low-levelとHigh-levelのインターフェイスを持った暗号化ライブラリです。Python 2.7, 3.5+に対応しています。こちらも共通鍵暗号からキー生成といった機能は一通り揃っています。

強調しているのは、「暗号化についてよくわからないのであれば、High-levelの方を使ってね」ということです。Low-levelのインターフェイスは誤って使用するとセキュリティ的なリスクが大きいため、hazardous material、略してhazmat層という名前で定義されています。

pyca/cryptographyで提供している共通鍵暗号では、Fernetを採用しています。Fernetは、キー以外で暗号化の際に加えるべき情報(バージョンやタイムスタンプなど)を定めた規格で、暗号化はCBCモードのAES128で実施されます。

こちらも軽く試してみました。インストールはpipから可能です。

pip install cryptography

Fernetをimportすれば、キー生成と暗号化が行えます。encryptメソッドを実行するとトークンが返されます。

>>> from cryptography.fernet import Fernet
>>> key = Fernet.generate_key()
>>> key
b'RguEWlPsq-A9MxQLb9FbFePV16d_DtTa0o4HKklhPvc='
>>> f = Fernet(key)
>>> token = f.encrypt(b"Target data")
>>> token
b'gAAAAABe3y1OEs7h9_uuvBUau6BHSa_yef_5Ac0v75yZ2R_FZpEyc-7GJG2nOlZRngINF74dMqLnMG3MEPO0pHsBLqKDQc0MCw=='

復号はdecryptメソッドにトークンを渡すだけ。

>>> f.decrypt(token)
b'Target data'

なるほど、PyCryptodomeよりpyca/cryptographyの方が幾分かシンプルですね。ただし、pyca/cryptographyが採用しているFernetのCBCモードのAES12について、PyCryptodomeのドキュメントに以下の記載がありました。

Classic modes of operation such as CBC only provide guarantees over the confidentiality of the message but not over its integrity. In other words, they don’t allow the receiver to establish if the ciphertext was modified in transit or if it really originates from a certain source.

CBCはconfidentiality(機密性)は保たれるけど、integrity(完全性)は保たれないらしいです。PyCryptodomeではCBCはクラシックと定義しているので、できればモダンなモードを採択した方がベター。

2020年6月現在で、PyCryptodomeでモダンなモードとして提供されているのは以下の5種類です。この辺りは連携するアプリでの対応状況やセキュリティ要件、暗復号の速度を加味して採択する必要があります。EASのモードの種類として、次のサイトが参考になりました。→How to choose an Authenticated Encryption mode – A Few Thoughts on Cryptographic Engineering

  • CCM (Counter Mode with CBC MAC)
  • EAX
  • GCM (Galois Counter Mode)
  • SIV (Synthetic Initialization Vector)
  • OCB (Offset CodeBook mode)

pyca/cryptographyにもLow-levelのインターフェイスはありますが、設計的にPyCryptodomeよりも考慮すべきことが多そうでした。GCMモードのAESを安全に使うために、今回のライブラリ選定ではPyCryptodomeを使うことにします。

まとめと比較表

2020年6月現在での、両ライブラリの共通鍵を比較した表を作成しました。ご参考になれば幸いです。

アルゴリズム PyCryptodome pyca/cryptography
AES
ARC4
Blowfish
Camellia
CAST-128
CAST-5
ChaCha20
IDEA
PKCS#1 v1.5 encryption (RSA)
RC2
Salsa20
SEED
Single DES
Triple DES
モード PyCryptodome pyca/cryptography
CBC
CCM
CFB
CFB8
CTR
EAX
ECB
GCM
OCB
OFB
OpenPGP
SIV
XTS

今回はAWS Lambda上でも使いたいので、ライブラリのサイズも合わせて調べてみました。どちらもサイズ的には問題ありません。

解凍済み Zip
Lambda Layerの上限 250 MB 50 MB
PyCryptodome 3.9.7 39.5 MB 14.1 MB
pyca/cryptography 2.9.2 8.4 MB 2.6 MB

参照