認証機能はWebサービスを作るときに重要ですが、パスワードの保管って厄介ですよね。Cookieやらスクリプトに平文を直書きするのは抵抗があります。
スクリプト実行時に入力させたり(getpass
モジュール)、OS依存のCredential管理機能を借用するという方法もあるようですが、Python以外と連携させるときなど場合によってはちょっと使いにくい。
そこで、デファクトな暗号化方式を扱えるようにしておくと、様々な言語でライブラリが提供されていたり、ネットで参考情報が見つかったりと、ところどころ便利です。
もくじ:
はじめに
万能というわけではないのですが、ここではPythonでAESを使うやり方をメモしておきます。
AESはいわゆる共通鍵暗号なので、平文をキー(暗号鍵)で暗号化し、キーを知っていれば、同じキーで暗号を平文へ復号できます。
例えば最初に書いたように、平文で秘密の情報を保存しておく必要はありませんし、他にもクライアントサーバ間通信などに使えると思います。送り手と受け手で同じキーを使う仕組みさえあれば、送り手のクライアントがデータを暗号化し、受け取ったサーバで復号することで、途中の伝送路には平文が流れることがありません。
当たり前ですが、キーがバレるとまずいです。
主要な暗号化方式はライブラリ化されており、Pythonではpycryptoが使えます。
標準ではないので、入っていない場合はいつものごとくpipでインストール。なお検証した環境はWindows10 + Python3.6です。
1 2 3 | > pip install pycrypto > python --version Python 3.6.1 |
参考にさせて頂いたページ。
あとStackOverflowもちょいちょい。
【追記】
Windows環境の場合、Visual Cランタイム関連のビルドエラーがあるためpycryptoよりもpycryptodomeのほうが推奨されています。モジュール名などは(ほぼ)同じです。
1 | > pip install pycryptodome |
PythonでのAES暗号化
こだわり始めると設定やら方式やらが色々あるのですが、まあパスワード程度の文字列が暗号化・復号できればいいので、決め打ちでハードコーディングしてしまいます。
ここではpycryptoを使ってAES暗号化を実装します(cryptoutil.py
)、というかうすーいラッパーですね。臆面もなくコードは参考サイトの丸パクリですが、アンパディング周りなど必要に応じて展開しているのと、キー生成用にクラスを定義しています。
まず、サンプル全体を記載しておきます。ふたつのクラスを定義しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | import pickle import base64 # padding by pkcs7 _pad = lambda s: s + (32 - len(s) % 32) * chr(32 - len(s) % 32) # save data to file def _save_file(file_path, data): target = file_path if file_path == "": target = "_{}.pkl".format("key" if type(data) == type("") else "pass") with open(target, 'wb') as f: pickle.dump({"data": data}, f) # load data from file def _load_file(file_path, iskey=False): target = file_path if file_path == "": target = "_{}.pkl".format("key" if iskey else "pass") if target.endswith("psk"): with open(target, 'r', encoding='utf-8-sig') as f: return f.read().replace("\r", "").replace("\n", "") else: with open(target, 'rb') as f: return pickle.load(f)["data"] class KeyUtil: # utility class for managing key def __init__(self, key=""): if key != "": # set secret key with modifying self.__key = key[:32] if len(key) >= 32 else _pad(key) else: # generate new key import random import string self.__key = ''.join(random.choices( string.ascii_letters + string.digits, k=32)) def get(self): return self.__key def save(self, file_path=""): _save_file(file_path, self.__key) def load(self, file_path=""): self.__key = _load_file(file_path, iskey=True) return self.__key class AesCipher: def __init__(self, secret_key): if len(secret_key) != 32: raise Exception("unexpected secret key length") self.__key = secret_key # use bytes in case of PyCryptodome self.__key = sectet_key.encode("utf-8") # --- self.__encrypted = None def encrypt_phrase(self, phrase): from Crypto import Random from Crypto.Cipher import AES iv = Random.new().read(AES.block_size) cipher = AES.new(self.__key, AES.MODE_CBC, iv).encrypt(_pad(phrase)) # store encrypted phrase self.__encrypted = base64.b64encode(iv + cipher) def get(self): return self.__encrypted def save(self, file_path=""): _save_file(file_path, self.__encrypted) def load(self, file_path=""): self.__encrypted = _load_file(file_path) def decrypt_phrase(self, encrypted_phrase=""): # use loaded one if argument is not specified data_bytes = encrypted_phrase if encrypted_phrase == "": data_bytes = self.__encrypted from Crypto.Cipher import AES enc = base64.b64decode(data_bytes) cipher = AES.new(self.__key, AES.MODE_CBC, enc[:AES.block_size]) dec = cipher.decrypt(enc[AES.block_size:]) # decode as unpad string return dec[:-ord(dec[len(dec) - 1:])].decode() |
実装は見たままなので、以下主に使い方の説明です。
キーの作成と管理
KeyUtil
はキーを管理するクラス。
インスタンス作成時に自動生成させるか、任意の文字列を渡してキーを作ります。キーは32文字(鍵長256bit)に固定ですので、任意の文字列を渡した場合には32文字より多い場合は余りを切り捨て、少ない場合はパディング(PKCS#7,暗号化のときも同じ)します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # Python対話シェル PS > python >>> from cryptoutil import KeyUtil # 自動生成して保存 >>> util = KeyUtil() >>> util.get() 'YSjPaZ7as2EvldwuwFhz1QQKEERVwG6D' >>> util.save() # 32文字以内の場合 >>> util = KeyUtil("secret key") >>> util.get() 'secret key\x16\x16\x16...(略)...\x16' # 32文字を越える場合 >>> util = KeyUtil("secret key with length over 32 letters") >>> util.get() 'secret key with length over 32 l' >>> exit() # 再度起動してロードしてみる PS > python >>> from cryptoutil import KeyUtil >>> util = KeyUtil() >>> util.load() 'YSjPaZ7as2EvldwuwFhz1QQKEERVwG6D' |
私はモノグサなのでキーをファイルに保存・読み込みする機能も付けていますが、もちろんキーと暗号は別に管理しないと意味がありません。
ファイル名を指定するか、指定しない場合はデフォルト(_key.pkl
)が適用されます。
暗号化と復号
上で作成したキーを読み込んで、暗号化を試します。暗号化した結果はBase64でエンコードしたものになり、復号メソッドに渡せばそのまま復号してくれます。
キーと暗号、それぞれを保存したファイルから復号するときは、下の4行で元の平文を取得できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | PS > python >>> from cryptoutil import KeyUtil, AesCipher >>> cip = AesCipher(KeyUtil().load()) # 平文を暗号化 >>> cip.encrypt_phrase("this is secret phrase") >>> cip.get() b'+draj/3FtBbNpVKkea/vmK58GEWejKwhaQsTmKFxBOvfHet1X6dQtu+O2/mjmEG2' # 復号 >>> cip.decrypt_phrase('+draj/3FtBbNpVKkea/vmK58GEWejKwhaQsTmKFxBOvfHet1X6dQtu+O2/mjmEG2') 'this is secret phrase' # 保存して一旦終了 >>> cip.save() >>> exit() # 保存された暗号を読み込んで復号 PS > python >>> from cryptoutil import KeyUtil, AesCipher >>> cip = AesCipher(KeyUtil().load()) >>> cip.load() >>> cip.decrypt_phrase() 'this is secret phrase' |
今回作成したクラスには暗号化・復号の両方のメソッドがありますが、もちろん役割が明確に決まっているスクリプトならどちらかに絞ってしまって構いません。
PowerShellでの暗号化
ここからは補足的な話。送り手(暗号化する側)がPowerShellだとどうでしょうか。
PowerShellスクリプトで何らかの処理をして、結果を暗号化してサーバに送信、サーバ側で復号する場合。サーバ側をPythonで実装すると、上の復号機能の部分がそのまま使えそうですね。
PowerShellでAES暗号化
AES暗号化を.NETの機能(System.Security
)を使って実装します。
ここでは、キー(文字列)と平文、ファイル名を渡すと暗号化した文字列を指定されたファイルに書き出す関数aes_encrypt
を定義しています。上で書いたように、本来はWeb API経由で送信するのがいいのですが、面倒なのでローカルのファイル経由にします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | function aes_encrypt($key, $plain_string, $file_name="ps_pass.psk"){ # check length if ($key.Length -ne 32){ Write-Host "key size does not match as 32" return } Add-Type -AssemblyName System.Security $aes = New-Object System.Security.Cryptography.AesCryptoServiceProvider $aes.KeySize = 256 $aes.BlockSize = 128 $aes.Mode = "CBC" $aes.Padding = "PKCS7" $aes.GenerateIV() $aes.Key = [System.Text.Encoding]::UTF8.GetBytes($key) # encryption $byte_pstr = [System.Text.Encoding]::UTF8.GetBytes($plain_string) $encryptor = $aes.CreateEncryptor() $data_byte = $aes.IV + $encryptor.TransformFinalBlock($byte_pstr, 0, $byte_pstr.Length) # dispose objects $encryptor.Dispose() $aes.Dispose() # save to file [System.Convert]::ToBase64String($data_byte) | Out-File $file_name -Encoding utf8 } |
なお、出力するファイルを簡単に識別させるため、拡張子を適当にpsk
にしました。
Pythonでの復号
同じスクリプトで復号するので特に変更点はありません。ここでは暗号化された文字列をPowerShellが出力したファイル(拡張子psk
)から読み込みます。
復号とは特に関係ありませんが、ファイルの読み方には若干注意が必要です。
対象のファイルをBOM付きUTF-8テキストファイルとして文字列を読み出し、念のためCRLFを除去します。
1 2 3 | if target.endswith("psk"): with open(target, 'r', encoding='utf-8-sig') as f: return f.read().replace("\r", "").replace("\n", "") |
簡単化のため以降はPowerShellコンソールで検証しています。
まずPythonのシェルでキーを作成し、保存します。ここまでは上と同じですが、キーを表示させてクリップボードにコピーしておきます。
1 2 3 4 5 6 7 | PS > python >>> from cryptoutil import KeyUtil >>> key = KeyUtil() >>> key.get() 'FoBzTXaeVh3gy8vzxYbZU70FRrcqStp2' >>> key.save() >>> exit() |
PowerShellコンソールで平文を暗号化します。関数をロードし、先の実行結果からコピペしたキーと、平文を指定して実行。ファイル名は指定せず、デフォルトの名前(ps_pass.psk
)で出力させます。念のためcat
(Get-Content
)すると、暗号が書き込まれているのがわかりますね。
1 2 3 4 | PS > . .\ps_encrypt.ps1 PS > aes_encrypt "FoBzTXaeVh3gy8vzxYbZU70FRrcqStp2" "password here" PS > cat .\ps_pass.psk V7ogv0apJfc0S9Aw9wwO5AIA1ndiNGHfUOSSBkLuWKY= |
保存しておいたキーで初期化し、復号してみましょう。PowerShellが出力したファイル名を指定すると、暗号をロードしてくれます。期待通りならPowerShellで指定した平文が返ってくると思います。
1 2 3 4 5 6 7 8 9 | PS > python >>> from cryptoutil import KeyUtil, AesCipher >>> key = KeyUtil().load() >>> key 'FoBzTXaeVh3gy8vzxYbZU70FRrcqStp2' >>> cipher = AesCipher(key) >>> cipher.load("ps_pass.psk") >>> cipher.decrypt_phrase() 'password here' |