はじめに
既出のネタですが、自分なりに調べて理解したことを書きます。
ここでは、暗号化やAESがなんであるかについては説明しませんし、説明できるほど詳しくもありません。
もし間違っている点があれば、コメントで指摘していただければ適宜修正致します。
動作テスト環境
pycrypto(外部ライブラリ)を使用していますので、予めpipなどでインストールしておいてください。
OS: Arch Linux (5.1.2-arch1-1-ARCH)
言語: Python 3.7.3
ライブラリ: pycrypto 2.6.1
ソースコード
my_aes.py
pythonのコードからインポートして使うためのファイルです。encrypt.py
コマンドラインツール: 標準入力からデータを受け取り、標準出力から暗号化されたデータを出力します。decrypt.py
コマンドラインツール: 標準入力から暗号化されたデータを受け取り、標準出力から復号化したデータを出力します。
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
from Crypto import Random
def create_aes(password, iv):
sha = SHA256.new()
sha.update(password.encode())
key = sha.digest()
return AES.new(key, AES.MODE_CFB, iv)
def encrypt(data, password):
iv = Random.new().read(AES.block_size)
return iv + create_aes(password, iv).encrypt(data)
def decrypt(data, password):
iv, cipher = data[:AES.block_size], data[AES.block_size:]
return create_aes(password, iv).decrypt(cipher)
import sys
import getpass
import my_aes
password = getpass.getpass('password> ')
password2 = getpass.getpass('confirm> ')
if password != password2:
print('Passwords do not match.')
sys.exit(0)
enc = my_aes.encrypt(sys.stdin.buffer.read(), password)
sys.stdout.buffer.write(enc)
import sys
import getpass
import my_aes
password = getpass.getpass('password> ')
dec = my_aes.decrypt(sys.stdin.buffer.read(), password)
sys.stdout.buffer.write(dec)
利用方法
以下、Unix環境を前提に説明します。Windows環境の方はコマンド部分の説明を適宜読み変えてください。
はじめに3つのファイル(my_aes.py, encrypt.py, decrypt.py)を同じディレクトリに保存しておいてください。
コマンドラインからファイルを暗号化
暗号化/復号化はByte列を別のByte列に変換する作業なので特定のファイル形式に依存するものではありません。
テキスト、画像、音楽、動画など基本的にファイルであればなんでも可能です。
実行するとパスワードと確認用パスワードを要求されるので、好きなパスワードを設定してください。
$ cat 暗号化したいファイル | python3 encrypt.py > 保存したいファイル名
password> # パスワード
confirm> # 確認用パスワード
コマンドラインからファイルを復号化
暗号化と逆のことをするだけです。拡張子を忘れずにつけましょう。
$ cat 暗号化されたファイル | python3 decrypt.py > 保存したいファイル名
password> # 上で設定したパスワード
具体例
試しに手元にあった動画を暗号化して、復号化した後正しく再生できるか確認してみました。(問題なし)
$ cat video.mp4 | python3 encrypt.py > enc
password>
confirm>
$ cat enc | python3 decrypt.py > video2.mp4
password>
pythonのコードからの利用
import my_aesしてそれぞれ、暗号化ならencrypt(データ, パスワード)、復号化ならdecrypt(暗号化データ, パスワード)すれば暗号化/復号化したデータを返してくれます。
パスワードがstr型なのに対して、データと返り値はbytes型であることに注意してください。
文字列(str型)を暗号化する場合は、'文字列'.encode()のようにしてbytes型に変換してください。逆に、復号化した文字列データ(bytes型)は.decode()を使ってstr型に変換できます。
.encode()と.decode()は引数を省略した場合はutf-8とみなされます。
encrypt.pyとdecrypt.pyが具体的な利用方法を示したものになっているのでそちらを参照してください。
ユースケース
そもそもなぜファイルを暗号化する必要があるのかですが、主に、
- ディスクに平文で置いておきたくないファイルがある
- 暗号化されていない通信で安全にファイルを転送したい
などが理由に挙げられるでしょう。
自分の場合は、pythonのコードから読み込むための”某金融機関のログインに必要な情報を示したjsonファイル”を念の為に暗号化しておきたいというものでした。この場合、
1.手元のアカウント情報を示したファイル(仮にaccount.json)をencrypt.pyで暗号化
$ cat account.json | python3 encrypt.py > account
2.pythonコード側(仮にmain.py)で、パスワードを要求して暗号されたファイル(account)を複合
3.復号化されたデータをJSONとしてパース(できなければパスワードが間違っている)
という流れになります。
import sys
import getpass
import json
import my_aes
password = getpass.getpass('password> ')
dec = my_aes.decrypt(open('account', 'rb').read(), password)
try:
account_info = json.loads(dec.decode())
except Exception:
print('Wrong password')
sys.exit(1)
# その後の処理
# account_id = account_info['account_id']
# password = account_info['password']
# print(account_id, password)
$ python3 main.py
password>
ソースコード解説
(※上のユースケースの様に使うだけであれば、この項を読む必要はありません。)
ここからはソースコードを理解する上でハマりそうな部分の解説をします。憶測が含まれているので過信しないでください。
ソースコードからも分かる通り、pycryptoは暗号化(AES)だけでなく乱数生成(Random)、ハッシュ(SHA256)の機能も備えています。
今回のメインはAESですが、RandomとSHA256をいい感じに利用した例になっていると思います。
大雑把な流れとしてはAES.new()でAESオブジェクトを生成し、それぞれ、暗号化なら.encrypt()、復号化なら.decrypt()にデータを渡すだけなのですが、コードの理解を難しくしているのがAES.new()に引数として渡されているkey(暗号化鍵)、AES.MODE_CFB(暗号化アルゴリズム)、iv(初期化ベクタ)でしょう。
暗号化鍵
まず暗号化鍵についてですがAESにおける鍵は固定長のため、パスワードをそのまま鍵にしてしまうと固定長のパスワードしか使えなくなってしまい不便です(しかも結構長い 128ビット・192ビット・256ビット)。
なので、ソースコード中のcerate_aes()では、パスワードをSHA256でハッシュした値(256bit)を鍵として利用しています。(注:この方法は安全ではありません。詳しくはコメントをご覧ください)
暗号化アルゴリズム
自分はAESが単一の暗号化アルゴリズムを指すものだと思っていたので、アルゴリズムを指定する引数があることが驚きだったのですが、どうやらそういうことらしいです。公式ドキュメントを見た所、5個ほどアルゴリズムがありました。
利用する上では「暗号化側と復号化側で同じアルゴリズムを用いる」、というところだけ気をつけていれば大丈夫だと思います。暗号化に詳しいわけではないのでこれ以上は踏み込まないでおきます。
初期化ベクタ
初期化ベクタはおそらく、乱数生成におけるシード値のようなものだと思います。
ソースコード中では初期化ベクタを乱数を使って生成していますが、これにより似通ったデータ(または完全に同一なデータ)であっても、暗号化する毎に全く違う暗号化データが生成されるため、データの同一性を隠すことができます。(サイズでバレます 笑)
もちろん、復号化側でも同じ初期化ベクタを利用する必要があるため、暗号化したデータの先頭に初期化ベクタをくっつけおきます。
これが一般的な方法かは知りませんが、公式ドキュメントではそのようにしていました。
ちなみに初期化ベクタは固定長で16byte(= AES.block_size)です。