PyCryptodome

【Python】PyCryptodomeによる暗号化・復号化

【Python】PyCryptodomeによる暗号化・復号化

Pythonで暗号化、復号化するためのモジュールであるPyCryptodomeモジュールについて解説します。共通鍵暗号方式のAES-GCMと公開鍵暗号方式のRSAを例にして説明します。

PyCryptodomeによる暗号化・復号化

Pythonでの暗号化・復号化については、pycyptoモジュールがよく知られているかと思います。本サイトでもこちらのページでpycryptoでの暗号化・復号化の使い方を紹介していましたが、リライトをしている際にpycryptoはもう古く、開発が既に終了しておりPyCryptodomeへの移行が推奨されていることを知りました。

PyCryptodomeは、pycryptoとほぼ同じAPIを公開しているため多くのアプリケーションは変更なしで実行できるようになっているようです。pycryptoとの互換性については公式ドキュメントのこちらを参考にしてください。

本記事ではPyCryptodomeでの暗号化・復号化について基本的な使い方を紹介します。

以降で紹介するプログラム例の動作確認は記事作成・更新時点で以下の動作条件で確認をしています。pythonバージョンやPyCryptodomeのバージョンによっては正常に動作しない可能性がある点はあらかじめご了承ください。

  • python 3.11.2
  • pycroptodome 3.17

PyCryptodomeを使用する場合には、以下のようにpip installでモジュールをインストールしておいてください。

pip install pycryptodome

暗号化方式には大きく共通鍵暗号方式と公開鍵暗号方式があります。以降では共通鍵暗号方式や公開鍵暗号方式によるPyCryptodomeでの暗号化・復号化について例を用いて紹介していきます。

なお、暗号理論の詳細は説明しませんので、各種暗号技術に関する書籍などを参照してもらえればと思います。

共通鍵暗号方式による暗号化・復号化

PyCryptodomeを用いた共通鍵暗号方式を用いた暗号化・復号化方法について紹介します。まずは、簡単に共通鍵暗号方式について説明します。

共通鍵暗号方式 概要図

共通鍵方式では、暗号化と復号化に共通の鍵を使用します。処理速度は速いことがメリットですが、通信接続先ごとに共通鍵を作成したり、第三者に共通鍵が盗聴されないように安全に交換しなければいけなかったりといったデメリットもあります。

共通鍵暗号方式としては、DES、3DES、AESといったものがあります。以降では共通鍵方式としてよく使用されているAESについてPyCryptodomeでの使い方を紹介します。

Advanced Encryption Standard (AES)

Advanced Encryption Standard (AES)は、通信データの暗号化などでよく使われている共通鍵暗号方式です。

AESにはいくつかのモードが利用できますが、以下例ではAESの認証付き暗号モードのGCM(Galois/Counter Mode)を使用します。認証付き暗号というのは復号時にデータが改ざんされていないかの完全性確認も同時にチェックできるモードです。

GCMはガロア認証+カウンター(CTR)モードという意味です。ガロア認証は暗号文が改ざんされていないことを検知する仕組みで、カウンターモードは従来からある暗号モードで処理が並列化可能で、パディングが不要な効率が良いモードです。

GCMでは認証タグ(TAG)というものがあり、暗号化する際に暗号文とTAGを一緒に生成します。復号の際には暗号文とTAGの両方が必要で、暗号文が改ざんされた場合には復号できなくなるため、機密性と完全性を担保できます。

以降では、PyCryptodomeによるAES-GCMでの暗号化、復号化のサンプルプログラムを紹介します。

実装例
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes


def main():
    # 暗号化対象文字列
    plaintext = "pycryptodomeを用いた文字列の暗号化・復号化"
    print(plaintext)
    # 文字列をバイトに変換
    plaintext_byte = plaintext.encode("utf-8")

    print("\n===== 暗号化 =====")
    # キーの生成
    key = get_random_bytes(16)
    # nonceの生成
    nonce = get_random_bytes(12)
    # 暗号化用オブジェクトの準備
    cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
    # 暗号化
    cipher_text, tag = cipher.encrypt_and_digest(plaintext_byte)
    print(f"暗号化文: {cipher_text}")
    print(f"TAG: {tag}")

    print("\n===== 復号化 =====")
    # 復号化用オブジェクトの準備
    cipher_dec = AES.new(key, AES.MODE_GCM, nonce=nonce)
    try:
        # 復号化
        decrypted_text = cipher_dec.decrypt_and_verify(cipher_text, tag)
        print(decrypted_text.decode())
    except (ValueError, KeyError) as ex:
        print(f"不正な復号 {ex}")


if __name__ == "__main__":
    main()
【実行結果例】
pycryptodomeを用いた文字列の暗号化・復号化

===== 暗号化 =====
暗号化文: b'K\xf3=\xc3\xcf\x03\x11\xf6`\x06\x7f&\xdf \x8a\x8b\x88u\x12V\x03\xd3i\xf5D\x13\xdbk\x14HtR\x85\xcf\xf3$3\x99\x0f\xecpvQds\xc0\xf3c\x1a\xd2\xdb\x19\xcd\xd1mX\x19'
TAG: b'\x1d\xfd\x8a\xde\xd5\xc7\x11h\x80\xd3\xba\x15\x90f\x9eS'

===== 復号化 =====
pycryptodomeを用いた文字列の暗号化・復号化
詳細説明
    # 暗号化対象文字列
    plaintext = "pycryptodomeを用いた文字列の暗号化・復号化"
    print(plaintext)
    # 文字列をバイトに変換
    plaintext_byte = plaintext.encode("utf-8")

上記の部分は暗号化対象の文字列の部分です。暗号化前には文字列をバイトに変換してください。

    print("\n===== 暗号化 =====")
    # キーの生成
    key = get_random_bytes(16)
    # nonceの生成
    nonce = get_random_bytes(12)
    # 暗号化用オブジェクトの準備
    cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
    # 暗号化
    cipher_text, tag = cipher.encrypt_and_digest(plaintext_byte)
    print(f"暗号化文: {cipher_text}")
    print(f"TAG: {tag}")

上記の部分が暗号化を実行している部分です。keyとnonceは、ランダムなバイトを生成しています。nonseとしては12バイト(96ビット)が一般的に使用されるようです。

暗号化用のオブジェクトはAES.newで生成します。その際にkeyやnonce、モードを指定します。GCMの場合はAES.MODE_GCMとなります。

暗号化はencrypt_and_digestに暗号化対象を引数として渡すことで実行ができ、暗号化文と認証タグが生成されます。

    print("\n===== 復号化 =====")
    # 復号化用オブジェクトの準備
    cipher_dec = AES.new(key, AES.MODE_GCM, nonce=nonce)
    try:
        # 復号化
        decrypted_text = cipher_dec.decrypt_and_verify(cipher_text, tag)
        print(decrypted_text.decode())
    except (ValueError, KeyError) as ex:
        print(f"不正な復号 {ex}")

上記の部分が復号化を実行している部分です。keyやnonceは安全に共有されているものと思ってください。

復号化用のオブジェクトでは、keyやnonceを使ってAES.newで生成します。復号化の際には、decrypt_and_verifyに暗号文と認証タグを渡します。

正常に復号化されればdecrypted_textに復号化文が設定されます。もし、暗号化文と認証タグが一致しない場合は、例外がexceptでキャッチされて「不正な復号 MAC check failed」のような出力がされます。

上記のようにすることでAESによる共通鍵暗号方式の暗号化・復号化が可能です。

公開鍵暗号方式による暗号化・復号化

PyCryptodomeを用いた公開鍵暗号方式を用いた暗号化・復号化方法について紹介します。まずは、簡単に公開鍵暗号方式について説明します。

公開鍵暗号方式 概要図

公開鍵暗号方式では、暗号化と復号化に別の鍵を使用します。暗号化に用いる鍵を公開鍵といい、復号化に用いる鍵を秘密鍵と言います。

公開鍵暗号では受信者側が公開鍵と暗号鍵のペアを生成し、公開鍵を受信者に共有します。送信者は公開鍵を使ってデータを暗号化し、受信者へ送信します。受信者は秘密鍵を使ってデータを復号化します。公開鍵暗号方式では、秘密鍵でしか復号化することができないため、公開鍵は第3者に知られても特に問題はありません。

公開鍵暗号方式は、通信接続先に関係なく1つペアを作成すればよいので鍵の管理が容易であったり、公開鍵を一般に公開するだけでよかったりといったメリットがありますが、共通鍵暗号方式に比べて処理が遅いというのがデメリットになります。そのため、鍵の交換のみ公開鍵暗号方式を使用して、データの暗号化は共通鍵暗号方式を使用するというハイブリッドな方式も使用されます。

公開鍵暗号方式としては、RSAやElGamalといったものがあります。以降では公開鍵方式として代表的なRSAについてPyCryptodomeでの使い方を紹介します。

RSA

RSA暗号は、桁数が大きい合成数の素因数分解が現実的な時間内で困難であることを安全性の根拠とした公開鍵暗号方式の一つです。発明者であるロナルド・リン・リベスト(Ronald Linn Rivest)、アディ・シャミア(Adi Shamir)、レオナルド・マックス・エーデルマン(Leonard Max Adleman)の頭文字をつなげてこのように呼ばれます。

以降では、PyCryptodomeによるRSAでの暗号化、復号化のサンプルプログラムを紹介します。

実装例

[generate_keys.py]

from Crypto.PublicKey import RSA


def main():
    # キーペアの生成
    key = RSA.generate(2048)
    # 秘密鍵
    private_key = key.export_key()
    with open("private.pem", "wb") as f:
        f.write(private_key)

    # 公開鍵の生成
    public_key = key.public_key().export_key()
    with open("public.pem", "wb") as f:
        f.write(public_key)


if __name__ == "__main__":
    main()

[rsa.py]

from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA


def main():
    # 暗号化対象文字列
    plaintext = "pycryptodomeを用いた文字列の暗号化・復号化"
    print(plaintext)
    # 文字列をバイトに変換
    plaintext_byte = plaintext.encode("utf-8")

    print("\n===== 暗号化 =====")
    # 公開鍵の読み込み
    with open("public.pem", "r") as f:
        public_key = RSA.import_key(f.read())

    # 暗号化用オブジェクトの準備
    cipher = PKCS1_OAEP.new(public_key)
    cipher_text = cipher.encrypt(plaintext_byte)
    print(f"暗号化文: {cipher_text}")

    print("\n===== 復号化 =====")
    # 秘密鍵の読み込み
    with open("private.pem", "r") as f:
        private_key = RSA.import_key(f.read())

    # 復号化用オブジェクトの準備
    cipher_dec = PKCS1_OAEP.new(private_key)
    decrypted_text = cipher_dec.decrypt(cipher_text)
    print(decrypted_text.decode())


if __name__ == "__main__":
    main()
【実行結果】秘密鍵と公開鍵がgenerate_keys.pyを実行し生成された後に実行
pycryptodomeを用いた文字列の暗号化・復号化

===== 暗号化 =====
暗号化文: b'\x17K\x8a\x92\xcf\x89c\xe9k\xf5\xaeEJ\x08\xb9\x8da\t\xe7\x1e\xd2\xa3TA\xb5b\x04f\xa3\xf0\x89\xac\xb1%\xa9^\'\x19\xdd\xed\xea\x92\xce\xd0\r\xcb\xc3S\xa5\x8d\x18A\x83!F\xe4\x85\xb7JV\x0b\xf3\xebQ\xe6\x16w\xb9\xec\xcb\x12\x00\xec\xf6"|-\x94p"\x00\x88\x19\xb1\xe2\x1b\x7f\x9d\xb7\\\x9f^\x1e\x01\'mP\x01N]\xce\xf2\x0f\x08Hff\xe2\x0fP\x19\xbf\x9b\xb1\x90v{\xee|\xed\xc2\xed\x88\xb1\x18+\xe0\xe7\xa4\x86z\x0b\xa5\t+\xf7\xe7\xe3\xf8\xce\xc5\xe1d\xbc\xe7\xa6\x19\xf6^9\x86\x8f<6\xc6I\xc5\x19\xca\xe8\xe0\xb3\xc4X&\xa6\xe4a\xbe\x18\x18@\x04\x7fE\x9b\xdd\x1e\xd7Hw\xde\x95\x12k\xc2\xca\xdd\x18\x18\xa0\xe8\x82~\xb2\xf6\n&\xac\x8e\xe3\xb4\xd9\xc6\x97\xb4=n\x82Qv\xd6\xab\xe9a\xa8!\xde\xc4\xd0y\xf9\xff\x12\x10n\xbb\xe0@\r\x135x\x13\xabJ\xeb\xbb&\xceH\xda\xf9\x9d\x8b\xd9\xf1\xb7\x95\xc2\xed\xf7\x94\x89G^'

===== 復号化 =====
pycryptodomeを用いた文字列の暗号化・復号化
詳細説明
    # キーペアの生成
    key = RSA.generate(2048)
    # 秘密鍵
    private_key = key.export_key()
    with open("private.pem", "wb") as f:
        f.write(private_key)

    # 公開鍵の生成
    public_key = key.public_key().export_key()
    with open("public.pem", "wb") as f:
        f.write(public_key)

各キーペアの生成は、generate_keys.py内で実行しています。

キーは、RSA.generateで生成します。引数はキーの長さで少なくとも1024である必要がありますが、2048が公式ドキュメントなどでもおすすめされています。

生成したkeyのexport_key()により秘密鍵が生成でき、private.pemというファイルで保存しています。また、公開鍵は生成したkeyのpublic_key().export_key()により生成でき、public.pemというファイルで保存しています。

このプログラムは受信者側で実施され、公開鍵ファイルについては公開されて送信者が使用します。

以降は、生成した公開鍵を使って暗号化、秘密鍵を使って復号化しているrsa.pyの方の処理を説明します。

    # 暗号化対象文字列
    plaintext = "pycryptodomeを用いた文字列の暗号化・復号化"
    print(plaintext)
    # 文字列をバイトに変換
    plaintext_byte = plaintext.encode("utf-8")

上記の部分は暗号化対象の文字列の部分です。暗号化前には文字列をバイトに変換してください。

    print("\n===== 暗号化 =====")
    # 公開鍵の読み込み
    with open("public.pem", "r") as f:
        public_key = RSA.import_key(f.read())

    # 暗号化用オブジェクトの準備
    cipher = PKCS1_OAEP.new(public_key)
    cipher_text = cipher.encrypt(plaintext_byte)
    print(f"暗号化文: {cipher_text}")

上記の部分が暗号化を実行している部分です。こちらは送信側で受信者が公開している公開鍵を読み込んで暗号化を実施している部分であると思ってください。

暗号化用オブジェクトとしてはPKCS1_OAEP.new公開鍵を指定して生成します。PKCS1_OAEPは、RSAとOAEPパディングに基づく非対称暗号と呼ばれるものです。

暗号化は、生成したオブジェクトのencryptに暗号化対象の文を渡すことで暗号化をすることができます。

   print("\n===== 復号化 =====")
    # 秘密鍵の読み込み
    with open("private.pem", "r") as f:
        private_key = RSA.import_key(f.read())

    # 復号化用オブジェクトの準備
    cipher_dec = PKCS1_OAEP.new(private_key)
    decrypted_text = cipher_dec.decrypt(cipher_text)
    print(decrypted_text.decode())

上記の部分が復号化を実行している部分です。こちらは受信者が送信者から送られてきた暗号文を秘密鍵で復号化している部分であると思ってください。

ほとんど暗号化の時と同じような構成になっていますが「鍵が秘密鍵である」点と「decryptを使用している点」が異なっています。実行結果を見てもわかるように公開鍵で暗号化した暗号文が、秘密鍵で復号化できていることが分かるかと思います。

上記のようにすることでRSAによる公開鍵暗号方式の暗号化・復号化が可能です。

まとめ

Pythonで暗号化、復号化するためのモジュールであるPyCryptodomeモジュールについて解説しました。

暗号化・復号化モジュールとしてはpycryptoがよく知られていますが、既に開発が終了しているため、PyCryptodomeへの移行が推奨されているようです。PyCryptodomeは、pycryptoとほぼ同じAPIを公開しているため多くのアプリケーションは変更なしで実行できるかと思います。

本記事では、共通鍵暗号方式として代表的なAES-GCMと公開鍵暗号方式で代表的なRSAについて使用方法を紹介ししました。他にも使用方法はあるため、是非公式ドキュメントを確認しつつ使用してみてもらえればと思います。

Note

PyCryptodomeの公式ドキュメントはこちらを参照してください。