Python не имеет встроенных схем шифрования, нет. Вы также должны серьезно относиться к хранению зашифрованных данных; тривиальные схемы шифрования, которые один разработчик считает небезопасными, и игрушечная схема может быть ошибочно принята за безопасную схему менее опытным разработчиком. Если вы шифруете, шифруйте правильно.
Однако для реализации правильной схемы шифрования вам не нужно много работать. Прежде всего, не изобретайте колесо криптографии , используйте доверенную библиотеку криптографии, чтобы справиться с этим за вас. Для Python 3 эта доверенная библиотека - cryptography
.
Я также рекомендую, чтобы шифрование и дешифрование применялось к байтам ; сначала кодировать текстовые сообщения в байты; stringvalue.encode()
кодирует в UTF8, легко восстанавливается снова с помощью bytesvalue.decode()
.
И последнее, но не менее важное: при шифровании и дешифровании мы говорим о ключах , а не о паролях. Ключ не должен быть человеком запоминающимся, это что-то, что вы храните в секретном месте, но машиночитаемое, тогда как пароль часто может быть читаем и запомнен человеком. Вы можете получить ключ из пароля, с небольшой осторожностью.
Но для того, чтобы веб-приложение или процесс, работающий в кластере без участия человека, продолжал его запускать, вам нужно использовать ключ. Пароли предназначены для тех случаев, когда только конечному пользователю необходим доступ к конкретной информации. Даже тогда вы обычно защищаете приложение паролем, а затем обмениваетесь зашифрованной информацией с помощью ключа, возможно, одного из них, прикрепленного к учетной записи пользователя.
Симметричный ключ шифрования
Fernet - AES CBC + HMAC, настоятельно рекомендуется
Библиотека cryptography
содержит рецепт Fernet , наилучший рецепт использования криптографии. 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'
Fernet с паролем - ключ, полученный из пароля, несколько ослабляет безопасность
Вы можете использовать пароль вместо секретного ключа, при условии, что вы используете метод деривации сильного ключа . Затем необходимо включить соль и число итераций HMAC в сообщение, чтобы зашифрованное значение больше не было совместимо с Fernet без предварительного разделения соли, счетчика и токена 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'
Включение соли в выводе позволяет использовать случайное значение соли, что, в свою очередь, гарантирует, что зашифрованный вывод гарантированно будет полностью случайным независимо от повторного использования пароля или повторения сообщения. Включение счетчика итераций обеспечивает возможность повышения производительности ЦП с течением времени, не теряя возможности расшифровывать старые сообщения.
Aодин пароль может быть таким же безопасным, как 32-байтовый случайный ключ Fernet, при условии, что вы генерируете случайный пароль из пула аналогичного размера.32 байта дают 256 ^ 32 количества ключей, поэтому если вы используете алфавит из 74 символов (26 верхних, 26 нижних, 10 цифр и 12 возможных символов), тогда ваш пароль должен быть длиной не менее math.ceil(math.log(256 ** 32, 74))
== 42 символа,Тем не менее, правильно выбранное большее количество итераций HMAC может несколько уменьшить недостаток энтропии, поскольку это значительно увеличивает стоимость атаки злоумышленником.
Просто знайте, чтовыбор более короткого, но все же достаточно надежного пароля не приведет к нарушению этой схемы, а лишь уменьшит количество возможных значений, которые злоумышленник должен будет найти;убедитесь, что вы выбрали достаточно надежный пароль для ваших требований безопасности .
Альтернативы
Затенение
Альтернативой является не шифровать .Не поддавайтесь искушению просто использовать шифр с низким уровнем безопасности или реализацию, созданную в домашних условиях, скажем Виньере.В этих подходах нет защиты, но они могут дать неопытному разработчику, которому поручено поддерживать ваш код в будущем, иллюзию безопасности, которая хуже, чем отсутствие безопасности вообще.
Если все, что вам нужно, этонеизвестность, просто 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='
.
Только целостность
Если все, что вам нужноэто способ убедиться, что данные можно доверять как без изменений после того, как они были отправлены ненадежному клиенту и получены обратно, затем вы хотите подписать данные, вы можете использовать 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 для обеспечения целостностизашифрованные данные;злонамеренный злоумышленник не может передать ваши системные бессмысленные данные, чтобы ваша служба работала в кругах с неправильным вводом, поскольку зашифрованный текст подписан.
Блочный шифр режима Galois / Counter создает зашифрованный тексти тег для той же цели, поэтому может использоваться для тех же целей.Недостатком является то, что в отличие от 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, но без необходимости дополнения
ЭтоПодход, который Все Vaiíty следует, хотя и неправильно.Это версия cryptography
, но обратите внимание, что я включаю IV в зашифрованный текст , он не должен храниться как глобальный (повторное использование IV ослабляет безопасность ключа и сохраняет его как модульglobal означает, что он будет заново сгенерирован при следующем вызове 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()
Thотсутствует добавленное сохранение подписи HMAC и нет отметки времени; Вы должны добавить их сами.
Вышеприведенное также иллюстрирует, как легко неправильно комбинировать базовые блоки криптографии; Все неправильные обращения со значением 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
может работать с общими криптографическими строительными блоками, даже теми, которые вы на самом деле не должны использовать.