本記事で紹介していたpycryptoの開発は終了しており、PyCryptodomeへの移行が推奨されているようです。PyCryptodomeでの暗号化・復号化について「PyCryptodomeによる暗号化・復号化」で紹介しておりますので、そちらのページを参考にしていただくことをおすすめします。
Pythonで暗号化、復号化するためのモジュールであるpycryptoモジュールについて解説します。暗号化の方法としては、共通鍵暗号アルゴリズムであるAdvanced Encryption Standard(AES)を取り上げます。
Contents
Advanced Encryption Standard(AES)
暗号化の方法としては、様々な方法があります。本記事ではAdvanced Encryption Standard(以降、AES)による暗号化・復号化をPythonでどのように実装するかについて解説します。
AESは、通信データの暗号化などでよく使われている共通鍵暗号方式です。
米国の国立標準技術研究所(NIST)が1997年に、当時標準的に使用されていたData Encryption Standard(以降、DES)の安全性の低下を理由にDESに代わる共通鍵暗号を募集しました。その時に集まった案がもととなり、AESが標準的な共通鍵暗号アルゴリズムとなっています。
この記事の目的はPythonでAESでどのように実現するかの紹介であるため、具体的な暗号化の理論などは紹介はしませんのでご注意ください。
pycryptoによる暗号化・復号化
Pythonで暗号化・復号化を実装するには、pycryptoモジュールを使用します。
pycryptoを使用する場合には、pip installで以下のようにモジュールをインストールするようにしてください。
pip install pycrypto
使用環境により以下のようなエラーでインストールできない場合があります。
error: Microsoft Visual C++ 14.0 or greater is required. Get it with "Microsoft C++ Build Tools": https://visualstudio.microsoft.com/visual-cpp-build-tools/
発生した場合の対応方法例を「【python】pip installの際に「error: Microsoft Visual C++ 14.0 is required」が発生した場合の対応方法」にまとめていますので参考にしていただければと思います。
pycryptoによるAESでの暗号化・復号化
pycryptoによるAESでの暗号化、復号化のサンプルプログラムを紹介します。このプログラムは、対象の文字列であるplaintextに対してAESで暗号化、復号化する例となっています。
import string import random from Crypto.Cipher import AES # 対象文字列 plaintext = 'Pythonのpycryptoによる文字列の暗号化・復号化' print(f'対象文字列:{plaintext}') print(f'AESブロックサイズ:{AES.block_size}') print(f'ASCII文字:{string.ascii_letters}') # AESブロックサイズ分のランダムなASCII文字列を作成する key = ''.join( [random.choice(string.ascii_letters) for _ in range(AES.block_size)]) print(f'key: {key}') # 初期ベクトル(initialization vector)を生成 iv = ''.join( [random.choice(string.ascii_letters) for _ in range(AES.block_size)]) print(f'iv: {iv}') print('===== 暗号化 =====') # 暗号化用オブジェクトの準備 cipher = AES.new(key, AES.MODE_CBC, iv) # バイト数がブロックサイズの倍数になるように詰め物(padding)の長さを計算する padding_len = AES.block_size - len(plaintext.encode('UTF-8')) % AES.block_size # 対象文字列にpadding長だけ文字列を追加する plaintext += chr(padding_len) * padding_len print(f'暗号化対象文字列:{plaintext}') print(f'暗号化対象文字列バイト長:{len(plaintext.encode("UTF-8"))}') # 文字列を暗号化する cipher_text = cipher.encrypt(plaintext) print(f'暗号化文字列:{cipher_text}') print('===== 復号化 =====') # 復号化用オブジェクトの準備 cipher_dec = AES.new(key, AES.MODE_CBC, iv) # 復号化を実施 decrypted_text = cipher_dec.decrypt(cipher_text) print(f'復号化直後:{decrypted_text.decode()}') decrypted_text = decrypted_text[:-decrypted_text[-1]] print(f'padding文字列除去:{decrypted_text.decode()}')
【実行結果例】 対象文字列:Pythonのpycryptoによる文字列の暗号化・復号化 AESブロックサイズ:16 ASCII文字:abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ key: nfhYHXUICpGJYRyb iv: peszfRClczkFKhlO ===== 暗号化 ===== 暗号化対象文字列:Pythonのpycryptoによる文字列の暗号化・復号化 暗号化対象文字列バイト長:64 暗号化文字列:b'\xebpf\xdcZ\x14\xd9\xfcJ\x89\x87\x98\xeb\xce?\xa6\xf8\x02a\xed\x84\xc6\xbc\xd5\xcf8\xce\x81\x04\x06\x1d\xb5\x1b\xf4EG\x88\xefrl\x8a\xdd\xf8\xc3\xe5\xf2\xae\x03\xb4\xa7j\x83=\xc9{q\xd1h\x82 j6\xaa\x00' ===== 復号化 ===== 復号化直後:Pythonのpycryptoによる文字列の暗号化・復号化 padding文字列除去:Pythonのpycryptoによる文字列の暗号化・復号化
※key, ivは実行時にランダムに生成しているため実行結果は上記と全く同じにはなりません。
以降で、プログラムのポイントについて解説していきます。
暗号化のための準備 (暗号化キーと初期化ベクトルの生成)
import string import random from Crypto.Cipher import AES
AES暗号化のために、Crypto.CipherからAESをインポートします。また、ランダムな文字列を生成するために、stringとrandomについてもインポートしています。
print(f'AESブロックサイズ:{AES.block_size}') print(f'ASCII文字:{string.ascii_letters}') # AESブロックサイズ分のランダムなASCII文字列を作成する key = ''.join( [random.choice(string.ascii_letters) for _ in range(AES.block_size)]) print(f'key: {key}') # 初期ベクトル(initial vector)を生成 iv = ''.join( [random.choice(string.ascii_letters) for _ in range(AES.block_size)]) print(f'iv: {iv}')
AESで暗号化・復号化するためには共通鍵となるkeyが必要になります。keyはAESブロックサイズ長である必要がありますが、AESのブロックサイズ長はAES.block_sizeで取得できます。
keyは、ASCII文字列からランダムに1文字ずつ、AESブロックサイズ長だけ取り出したリストをjoinで結合して作成しています。
ivというのは、初期化ベクトル(initialization vector)と呼ばれるものです。同じ文字列を同じ暗号鍵で暗号化すると毎回同じ結果になってしまいます。初期化ベクトルをランダムに変えることで、同じ文字列、同じ暗号鍵を使っていても毎回暗号化結果を変えることができます。
ivについての生成方法は、keyと同じです。
暗号化(encrypt)
# 暗号化用オブジェクトの準備 cipher = AES.new(key, AES.MODE_CBC, iv)
暗号化する場合には、AES.newでkey, ivを指定して暗号化用オブジェクトを生成します。AES.MODE_CBCというのは、CBCモードという暗号化モードのことで、平文の各ブロックは前の暗号文とのXORをとって暗号化されるモードです。
# バイト数がブロックサイズの倍数になるように詰め物(padding)の長さを計算する padding_len = AES.block_size - len(plaintext.encode('UTF-8')) % AES.block_size # 対象文字列にpadding長だけ文字列を追加する plaintext += chr(padding_len) * padding_len print(f'暗号化対象文字列:{plaintext}') print(f'暗号化対象文字列バイト長:{len(plaintext.encode("UTF-8"))}')
AESで暗号化する場合は、暗号化対象の文字列は、AESブロックサイズの倍数であるバイト数である必要があります。
そのため、パディング(padding)で不足する文字列数分を追加します。パディングは「詰め物」といった意味を持っています。
padding_lenのところでは、AESブロックサイズの倍数にするための文字列長を計算しています。そして、文字列長をchrで文字にして、padding_len数分だけ対象文字列の末尾に追加しています。
復号化のところで分かりますが、この文字列長をバイト文字にして追加している点がポイントです。この文字列長は復号化時にパディングで追加した文字列を除外する際に使用します。
注意点として、ASCII文字のみであれば問題ないのですが、マルチバイトを含む文字列だとlen(plainetext)のようにしてしまうとうまくいきません。encodeでバイトにしてから長さの調整の計算をするようにしましょう。
# 文字列を暗号化する cipher_text = cipher.encrypt(plaintext) print(f'暗号化文字列:{cipher_text}')
最後に、パディングした文字列をencryptメソッドに渡すことで、文字列を暗号化することができます。
復号化(decrypt)
# 復号化用オブジェクトの準備 cipher_dec = AES.new(key, AES.MODE_CBC, iv) # 復号化を実施 decrypted_text = cipher_dec.decrypt(cipher_text)
復号化する側の処理としては、暗号化した場合と同じkey, ivを指定してオブジェクトを作るところは同じです。
復号化処理は、暗号化されている文字列をdecryptメソッドに渡すことで復号化することができます。
print(f'復号化直後:{decrypted_text.decode()}') decrypted_text = decrypted_text[:-decrypted_text[-1]] print(f'padding文字列除去:{decrypted_text.decode()}')
ただし、復号化しただけで終えてしまうとパディングで追加した文字列の分が余分な文字列として入ってしまっています。そのため、復号化した文字列からパディング文字を除去する必要があります。
復号化した際にはパディング文字としてパディング文字列長を追加していますので復号化文字列の末尾に格納されているdecrypted_text[-1]で取得できる数字はパディング文字列長です。
そのため、decrypted_text = decrypted_text[:-decrypted_text[-1]]の部分では、復号化文字列の先頭からパディング文字列の前までの抽出しています。
上記のように復号化した文字列を取得することができます。
上記で紹介しているソースコードについてはgithubにて公開しています。参考にしていただければと思います。