У Python немає вбудованих схем шифрування, немає. Ви також повинні серйозно поставитися до зберігання зашифрованих даних; тривіальні схеми шифрування, які один розробник вважає небезпечними, а схема іграшок може бути помилково прийнята за безпечну схему менш досвідченим розробником. Якщо ви шифруєте, шифруйте належним чином.
Для впровадження правильної схеми шифрування не потрібно робити багато роботи. Перш за все, не вигадуйте колесо криптографії , використовуйте довірену бібліотеку криптовалют, щоб впоратися з цим. Для Python 3 ця надійна бібліотека є cryptography
.
Я також рекомендую, щоб шифрування та дешифрування стосувалися байтів ; кодувати текстові повідомлення спочатку в байти; stringvalue.encode()
кодує до UTF8, легко повертається знову за допомогою bytesvalue.decode()
.
Не в останню чергу, при шифруванні та розшифровці ми говоримо про ключі , а не паролі. Ключ не повинен бути запам'ятовуваним людиною, це щось, що ви зберігаєте в секретному місці, але машиночитабельне, тоді як пароль часто може бути прочитаний і запам'ятований людиною. Ви можете отримати ключ із пароля, дотримуючись певної обережності.
Але для того, щоб веб-додаток або процес, запущений у кластері без уваги людини, продовжував працювати, потрібно використовувати ключ. Паролі призначені для того, коли лише кінцевому користувачеві потрібен доступ до певної інформації. Навіть тоді ви зазвичай захищаєте програму паролем, а потім обмінюєтесь зашифрованою інформацією за допомогою ключа, можливо, такого, який додається до облікового запису користувача.
Симетричне шифрування ключа
Фернет - AES CBC + HMAC, настійно рекомендується
cryptography
Бібліотека включає рецепт Fernet , рецепт передових методів для використання криптографії. Фернет є це відкритий стандарт із готовими реалізаціями в широкому діапазоні мов програмування, і він пакує AES CBC-шифрування для вас інформацією про версію, часовою позначкою та підписом HMAC для запобігання підробці повідомлень.
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'
Фернет з паролем - ключ, отриманий із пароля, дещо послаблює захист
Ви можете використовувати пароль замість секретного ключа за умови використання сильного методу виведення ключа . Тоді вам доведеться включати в повідомлення сіль та кількість ітерацій HMAC, тому зашифроване значення більше не сумісне з Фернетом без попереднього розділення солі, підрахунку та маркера Fernet:
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'
Включення солі у висновок дає можливість використовувати випадкове значення солі, що, в свою чергу, гарантує, що зашифрований вихід гарантовано є повністю випадковим, незалежно від повторного використання пароля чи повторення повідомлення. У тому числі кількість ітерацій гарантує, що ви можете з часом налаштувати продуктивність процесора, не втрачаючи можливості розшифровувати старі повідомлення.
Сам пароль може бути таким же безпечним, як і 32-байтовий випадковий ключ Fernet, за умови, що ви генеруєте належним чином випадковий пароль із пулу аналогічних розмірів. 32 байти дає 256 ^ 32 кількість клавіш, тому якщо ви використовуєте алфавіт із 74 символів (26 верхніх, 26 нижніх, 10 цифр та 12 можливих символів), то ваш пароль повинен бути не менше math.ceil(math.log(256 ** 32, 74))
== 42 символів. Однак, a вдало підібрана більша кількість ітерацій HMAC може дещо пом’якшити відсутність ентропії, оскільки це набагато дорожче для зловмисника змусити пробитися.
Просто знайте, що вибір коротшого, але все-таки досить безпечного пароля не призведе до каліцтва цієї схеми, це просто зменшує кількість можливих значень, через які зловмисникові доведеться шукати; не забудьте вибрати досить надійний пароль для ваших вимог безпеки .
Альтернативи
Затемнення
Альтернативою є не шифрування . Не спокушайтеся просто використовувати шифр із низьким рівнем захисту або реалізацію домашньої розкрутки, скажімо Віньєре. У цих підходах немає ніякої безпеки, але це може надати недосвідченому розробнику, якому дано завдання підтримувати ваш код у майбутньому ілюзією безпеки, що гірше, ніж взагалі ніякої безпеки.
Якщо все, що вам потрібно, - це неясність, просто зафіксуйте дані64; для вимог щодо безпеки 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='
.
Тільки цілісність
Якщо все, що вам потрібно, це спосіб переконатися, що дані можуть бути недостовірними після того, як вони будуть надіслані ненадійним клієнтом і отримані назад, тоді ви хочете підписати дані, ви можете використовувати hmac
бібліотеку для цього з SHA1 (все ще вважається безпечним для підписання 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 ґрунтується на AEC-CBC з підписом HMAC для забезпечення цілісності зашифрованих даних; зловмисний зловмисник не може подати дані вашої системної дурниці, щоб ваш сервіс був зайнятий в кругах з неправильним введенням, оскільки шифротекст підписаний.
Блоковий режим Галуа / Лічильник шифр виробляє шифротекст і тег , щоб служити тієї ж мети, тому можуть бути використані , щоб служити тієї ж мети. Мінус полягає в тому, що на відміну від 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, але без потреби прокладки
Це такий підхід, якого всі Іѕ Ваііти дотримуються , хоча і неправильно. Це cryptography
версія, але зауважте, що я включаю 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 втрачено. Використання Fernet натомість захищає вас від таких помилок.
AES ECB - не захищено
Якщо ви раніше реалізували шифрування AES ECB і вам потрібно все-таки підтримувати це в Python 3, ви можете зробити це ще й із цим cryptography
. Це ж застереження застосовується, ЄЦБ недостатньо захищений для реальних програм . Повторно реалізуючи цю відповідь для 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
може обробляти загальні криптографічні будівельні блоки, навіть ті, якими ви не повинні користуватися.