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'
Фернет с паролем - ключ, полученный из пароля, несколько снижает безопасность
Вы можете использовать пароль вместо секретного ключа при условии, что вы используете надежный метод получения ключа . Затем вам нужно включить соль и счетчик итераций 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'
Включение соли в вывод позволяет использовать случайное значение соли, что, в свою очередь, гарантирует, что зашифрованный вывод гарантированно будет полностью случайным независимо от повторного использования пароля или повторения сообщения. Включение счетчика итераций гарантирует, что вы можете настроить увеличение производительности ЦП с течением времени, не теряя возможности расшифровывать старые сообщения.
Один только пароль может быть таким же безопасным, как 32-байтовый случайный ключ Fernet, при условии, что вы сгенерируете правильно случайный пароль из пула аналогичного размера. 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='
.
Только честность
Если все, что вам нужно, это способ убедиться, что данные можно доверять, чтобы они не были изменены после того, как они были отправлены ненадежному клиенту и получены обратно, тогда вы хотите подписать данные, вы можете использовать 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, но без подкладки
Это подход, которому следует All Іs Vаиітy , хотя и неправильно. Это 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 и временная метка; вам придется добавить их самостоятельно.
Вышеупомянутое также показывает, насколько легко неправильно комбинировать базовые строительные блоки криптографии; Неправильная обработка значения IV Vaiti может привести к утечке данных или к нечитаемости всех зашифрованных сообщений из-за потери 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
может обрабатывать обычные криптографические строительные блоки, даже те, которые вам на самом деле не следует использовать.