Pythonには組み込みの暗号化スキームはありません。また、暗号化されたデータストレージを真剣に考えるべきです。1人の開発者が安全でないと理解している簡単な暗号化スキームとおもちゃのスキームは、経験の浅い開発者が安全なスキームと間違える可能性があります。暗号化する場合は、適切に暗号化してください。
ただし、適切な暗号化スキームを実装するために多くの作業を行う必要はありません。まず、暗号化ホイールを再発明せずに、信頼できる暗号化ライブラリを使用してこれを処理します。Python 3の場合、その信頼できるライブラリはcryptography
です。
暗号化と復号化をバイトに適用することもお勧めします。最初にテキストメッセージをバイトにエンコードします。stringvalue.encode()
UTF8にエンコードし、を使用して簡単に再び元に戻すことができbytesvalue.decode()
ます。
最後に重要なことですが、暗号化と復号化では、パスワードではなくキーについて説明します。鍵は人間が覚えやすいものではなく、秘密の場所に保存されているもので、機械が読み取れるものです。一方、パスワードは人間が読み取ったり記憶したりすることができます。少し注意して、パスワードからキーを派生させることができます。
しかし、人間の注意を払わずにクラスターで実行されているWebアプリケーションまたはプロセスを実行し続けるには、キーを使用します。パスワードは、エンドユーザーだけが特定の情報にアクセスする必要がある場合に使用します。それでも、通常はパスワードでアプリケーションを保護し、ユーザーアカウントに関連付けられているキーを使用して暗号化された情報を交換します。
対称鍵暗号化
Fernet – AES CBC + HMAC、強く推奨
cryptography
ライブラリは、Fernetレシピ、暗号を使用するためのベスト・プラクティスのレシピを。Fernetはオープンスタンダードであり、幅広いプログラミング言語ですぐに実装でき、バージョン情報、タイムスタンプ、およびHMAC署名とともにAES CBC暗号化をパッケージ化して、メッセージの改ざんを防止します。
Fernetは、暗号化と復号化のメッセージを非常に簡単にそれを作ると、あなたがセキュアに保ちます。シークレットでデータを暗号化するための理想的な方法です。
を使用Fernet.generate_key()
して安全な鍵を生成することをお勧めします。パスワードも使用できます(次のセクション)が、完全な32バイトの秘密鍵(暗号化に16バイト、さらに署名用に16バイト)は、考えられるほとんどのパスワードよりも安全です。
Fernetが生成するキーは、bytes
URLとファイルセーフのbase64文字を持つオブジェクトなので、印刷可能です。
from cryptography.fernet import Fernet
key = Fernet.generate_key() # store in a secure location
print("Key:", key.decode())
メッセージを暗号化または復号化するにFernet()
は、指定されたキーでインスタンスを作成し、Fernet.encrypt()
またはを呼び出します。Fernet.decrypt()
暗号化するプレーンテキストメッセージと暗号化されたトークンの両方がbytes
オブジェクトです。
encrypt()
そして、decrypt()
機能は次のようになります。
from cryptography.fernet import Fernet
def encrypt(message: bytes, key: bytes) -> bytes:
return Fernet(key).encrypt(message)
def decrypt(token: bytes, key: bytes) -> bytes:
return Fernet(key).decrypt(token)
デモ:
>>> key = Fernet.generate_key()
>>> print(key.decode())
GZWKEhHGNopxRdOHS4H4IyKhLQ8lwnyU7vRLrM3sebY=
>>> message = 'John Doe'
>>> encrypt(message.encode(), key)
'gAAAAABciT3pFbbSihD_HZBZ8kqfAj94UhknamBuirZWKivWOukgKQ03qE2mcuvpuwCSuZ-X_Xkud0uWQLZ5e-aOwLC0Ccnepg=='
>>> token = _
>>> decrypt(token, key).decode()
'John Doe'
パスワード付きのFernet –パスワードから派生したキーは、セキュリティを多少弱めます
強力な鍵導出方法を使用している場合は、秘密鍵の代わりにパスワードを使用できます。次に、メッセージにソルトとHMAC反復カウントを含める必要があるため、暗号化された値は、ソルト、カウント、およびフェルネットトークンを最初に分離しない限り、フェルネット互換ではなくなります。
import secrets
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d
from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
backend = default_backend()
iterations = 100_000
def _derive_key(password: bytes, salt: bytes, iterations: int = iterations) -> bytes:
"""Derive a secret key from a given password and salt"""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(), length=32, salt=salt,
iterations=iterations, backend=backend)
return b64e(kdf.derive(password))
def password_encrypt(message: bytes, password: str, iterations: int = iterations) -> bytes:
salt = secrets.token_bytes(16)
key = _derive_key(password.encode(), salt, iterations)
return b64e(
b'%b%b%b' % (
salt,
iterations.to_bytes(4, 'big'),
b64d(Fernet(key).encrypt(message)),
)
)
def password_decrypt(token: bytes, password: str) -> bytes:
decoded = b64d(token)
salt, iter, token = decoded[:16], decoded[16:20], b64e(decoded[20:])
iterations = int.from_bytes(iter, 'big')
key = _derive_key(password.encode(), salt, iterations)
return Fernet(key).decrypt(token)
デモ:
>>> message = 'John Doe'
>>> password = 'mypass'
>>> password_encrypt(message.encode(), password)
b'9Ljs-w8IRM3XT1NDBbSBuQABhqCAAAAAAFyJdhiCPXms2vQHO7o81xZJn5r8_PAtro8Qpw48kdKrq4vt-551BCUbcErb_GyYRz8SVsu8hxTXvvKOn9QdewRGDfwx'
>>> token = _
>>> password_decrypt(token, password).decode()
'John Doe'
出力にソルトを含めると、ランダムなソルト値を使用できるようになり、パスワードの再利用やメッセージの繰り返しに関係なく、暗号化された出力が完全にランダムになることが保証されます。反復カウントを含めることで、古いメッセージを復号化する機能を失うことなく、時間の経過とともにCPUパフォーマンスの向上に調整できます。
同様のサイズのプールから適切にランダムなパスワードを生成すれば、パスワードだけでも、Fernet 32バイトのランダムキーと同じくらい安全です。32バイトで256 ^ 32のキー数が得られるため、74文字のアルファベット(上26、下26、10桁、可能な記号12)を使用する場合、パスワードは少なくともmath.ceil(math.log(256 ** 32, 74))
== 42文字にする必要があります。ただし、適切に選択された多数のHMAC反復により、エントロピーの欠如をある程度緩和できます。これにより、攻撃者がブルートフォース攻撃を仕掛けるのが非常に高価になるためです。
短くても適度に安全なパスワードを選択しても、このスキームが機能しなくなることはありません。ブルートフォース攻撃者が検索しなければならない可能性のある値の数が減るだけです。セキュリティ要件に対応できる十分強力なパスワードを選択してください。
代替案
あいまい
代わりの方法は暗号化しないことです。Vignereは、セキュリティの低い暗号だけを使用したり、自作の実装を使用したりしないでください。これらのアプローチにはセキュリティはありませんが、コードを保守するタスクを経験の浅い開発者に与えて、セキュリティの幻想を与える可能性があります。
必要なのが不明瞭な場合は、データをbase64するだけです。URLセーフ要件の場合、base64.urlsafe_b64encode()
関数は問題ありません。ここではパスワードを使用せず、エンコードするだけで完了です。せいぜい、いくつかの圧縮を追加します(などzlib
):
import zlib
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d
def obscure(data: bytes) -> bytes:
return b64e(zlib.compress(data, 9))
def unobscure(obscured: bytes) -> bytes:
return zlib.decompress(b64d(obscured))
このターンb'Hello world!'
にb'eNrzSM3JyVcozy_KSVEEAB0JBF4='
。
完全性のみ
必要なのは、データが信頼できないクライアントに送信されて受信された後、データが変更されないように信頼できることを確認する方法だけである場合、データに署名する必要がある場合は、SHA1でこのhmac
ライブラリを使用できます(まだHMAC署名用に安全であると考えられている)以上:
import hmac
import hashlib
def sign(data: bytes, key: bytes, algorithm=hashlib.sha256) -> bytes:
assert len(key) >= algorithm().digest_size, (
"Key must be at least as long as the digest size of the "
"hashing algorithm"
)
return hmac.new(key, data, algorithm).digest()
def verify(signature: bytes, data: bytes, key: bytes, algorithm=hashlib.sha256) -> bytes:
expected = sign(data, key, algorithm)
return hmac.compare_digest(expected, signature)
これを使用してデータに署名し、署名をデータに添付して、クライアントに送信します。データを受け取ったら、データと署名を分割して検証します。デフォルトのアルゴリズムをSHA256に設定したので、32バイトの鍵が必要になります。
key = secrets.token_bytes(32)
これをさまざまな形式でシリアル化および非シリアル化してパッケージ化したitsdangerous
ライブラリを確認することをお勧めします。
AES-GCM暗号化を使用して暗号化と整合性を提供する
FernetはHMAC署名付きのAEC-CBC上に構築され、暗号化されたデータの整合性を保証します。暗号文が署名されているため、悪意のある攻撃者がシステムの無意味なデータをフィードして、不正な入力のあるサークルでサービスをビジー状態に保つことはできません。
ガロア/カウンタモードでブロック暗号は、暗号文と生成タグと同じ目的を果たすために、これと同じ目的を果たすために使用することができます。欠点は、Fernetとは異なり、使いやすい万能のレシピが他のプラットフォームで再利用できないことです。AES-GCMもパディングを使用しないため、この暗号化暗号文は入力メッセージの長さと一致します(Fernet / AES-CBCはメッセージを固定長のブロックに暗号化し、メッセージの長さを多少不明瞭にします)。
AES256-GCMは、通常の32バイトの秘密をキーとして使用します。
key = secrets.token_bytes(32)
次に使用します
import binascii, time
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.exceptions import InvalidTag
backend = default_backend()
def aes_gcm_encrypt(message: bytes, key: bytes) -> bytes:
current_time = int(time.time()).to_bytes(8, 'big')
algorithm = algorithms.AES(key)
iv = secrets.token_bytes(algorithm.block_size // 8)
cipher = Cipher(algorithm, modes.GCM(iv), backend=backend)
encryptor = cipher.encryptor()
encryptor.authenticate_additional_data(current_time)
ciphertext = encryptor.update(message) + encryptor.finalize()
return b64e(current_time + iv + ciphertext + encryptor.tag)
def aes_gcm_decrypt(token: bytes, key: bytes, ttl=None) -> bytes:
algorithm = algorithms.AES(key)
try:
data = b64d(token)
except (TypeError, binascii.Error):
raise InvalidToken
timestamp, iv, tag = data[:8], data[8:algorithm.block_size // 8 + 8], data[-16:]
if ttl is not None:
current_time = int(time.time())
time_encrypted, = int.from_bytes(data[:8], 'big')
if time_encrypted + ttl < current_time or current_time + 60 < time_encrypted:
# too old or created well before our current time + 1 h to account for clock skew
raise InvalidToken
cipher = Cipher(algorithm, modes.GCM(iv, tag), backend=backend)
decryptor = cipher.decryptor()
decryptor.authenticate_additional_data(timestamp)
ciphertext = data[8 + len(iv):-16]
return decryptor.update(ciphertext) + decryptor.finalize()
Fernetがサポートするのと同じ存続時間のユースケースをサポートするために、タイムスタンプを含めました。
このページの他のアプローチ、Python 3
AES CFB- CBCに似ていますが、パディングする必要がありません
これは、正しくありませんが、AllІѕVаиітyが従うアプローチです。これはcryptography
バージョンですが、IVを暗号文に含めます。IVをグローバルとして保存しないでください(IVを再利用すると、キーのセキュリティが低下します。モジュールをグローバルとして保存すると、再生成されます次のPython呼び出し、すべての暗号文を解読不能にする):
import secrets
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
backend = default_backend()
def aes_cfb_encrypt(message, key):
algorithm = algorithms.AES(key)
iv = secrets.token_bytes(algorithm.block_size // 8)
cipher = Cipher(algorithm, modes.CFB(iv), backend=backend)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(message) + encryptor.finalize()
return b64e(iv + ciphertext)
def aes_cfb_decrypt(ciphertext, key):
iv_ciphertext = b64d(ciphertext)
algorithm = algorithms.AES(key)
size = algorithm.block_size // 8
iv, encrypted = iv_ciphertext[:size], iv_ciphertext[size:]
cipher = Cipher(algorithm, modes.CFB(iv), backend=backend)
decryptor = cipher.decryptor()
return decryptor.update(encrypted) + decryptor.finalize()
これにはHMAC署名の追加のアーマーがなく、タイムスタンプはありません。それらを自分で追加する必要があります。
上記は、基本的な暗号化ビルディングブロックを誤って組み合わせることがいかに簡単かを示しています。ІѕVаиітyによるIV値の不適切な処理は、IVが失われるため、データ侵害またはすべての暗号化されたメッセージの読み取り不能につながる可能性があります。代わりにFernetを使用することで、そのような間違いからあなたを守ります。
AES ECB – 安全ではない
以前にAES ECB暗号化を実装していて、これをPython 3で引き続きサポートする必要がある場合も、同様にそれを行うことができますcryptography
。同じ警告が適用され、ECBは実際のアプリケーションに対して十分に安全ではありません。その答えをPython 3に再実装して、パディングの自動処理を追加します。
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.backends import default_backend
backend = default_backend()
def aes_ecb_encrypt(message, key):
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend)
encryptor = cipher.encryptor()
padder = padding.PKCS7(cipher.algorithm.block_size).padder()
padded = padder.update(msg_text.encode()) + padder.finalize()
return b64e(encryptor.update(padded) + encryptor.finalize())
def aes_ecb_decrypt(ciphertext, key):
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend)
decryptor = cipher.decryptor()
unpadder = padding.PKCS7(cipher.algorithm.block_size).unpadder()
padded = decryptor.update(b64d(ciphertext)) + decryptor.finalize()
return unpadder.update(padded) + unpadder.finalize()
繰り返しますが、これにはHMAC署名がないため、とにかくECBを使用しないでください。上記はcryptography
、実際に使用してはいけないものでも、一般的な暗号化ビルディングブロックを処理できることを示すためのものです。