Python no tiene esquemas de cifrado integrados, no. También debe tomarse en serio el almacenamiento de datos cifrados; esquemas de cifrado triviales que un desarrollador entiende que son inseguros y un esquema de juguete bien puede ser confundido con un esquema seguro por un desarrollador menos experimentado. Si encripta, encripte correctamente.
Sin embargo, no necesita hacer mucho trabajo para implementar un esquema de cifrado adecuado. En primer lugar, no reinvente la rueda de criptografía , use una biblioteca de criptografía confiable para manejar esto por usted. Para Python 3, esa biblioteca confiable es cryptography
.
También recomiendo que el cifrado y el descifrado se apliquen a los bytes ; codificar mensajes de texto en bytes primero; stringvalue.encode()
codifica a UTF8, fácilmente revertido nuevamente usando bytesvalue.decode()
.
Por último, pero no menos importante, a la hora de cifrar y descifrar, hablamos de claves , no de contraseñas. Una clave no debe ser memorable por humanos, es algo que usted almacena en un lugar secreto pero legible por máquina, mientras que una contraseña a menudo puede ser legible y memorizada por humanos. Usted puede obtener una clave de una contraseña, con un poco de cuidado.
Pero para una aplicación web o un proceso que se ejecuta en un clúster sin atención humana para seguir ejecutándolo, debe usar una clave. Las contraseñas son para cuando solo un usuario final necesita acceder a la información específica. Incluso entonces, normalmente protege la aplicación con una contraseña y luego intercambia información cifrada mediante una clave, tal vez una adjunta a la cuenta de usuario.
Cifrado de clave simétrica
Fernet - AES CBC + HMAC, muy recomendable
La cryptography
biblioteca incluye la receta de Fernet , una receta de mejores prácticas para usar la criptografía. Fernet es un estándar abierto , con implementaciones listas en una amplia gama de lenguajes de programación y empaqueta el cifrado AES CBC para usted con información de versión, una marca de tiempo y una firma HMAC para evitar la manipulación de mensajes.
Fernet hace que sea muy fácil cifrar y descifrar mensajes y mantenerlo seguro. Es el método ideal para cifrar datos con un secreto.
Te recomiendo que lo uses Fernet.generate_key()
para generar una clave segura. También puede usar una contraseña (siguiente sección), pero una clave secreta completa de 32 bytes (16 bytes para cifrar, más otros 16 para la firma) será más segura que la mayoría de las contraseñas que pueda imaginar.
La clave que genera Fernet es un bytes
objeto con URL y caracteres base64 seguros para el archivo, por lo que se puede imprimir:
from cryptography.fernet import Fernet
key = Fernet.generate_key() # store in a secure location
print("Key:", key.decode())
Para cifrar o descifrar mensajes, cree una Fernet()
instancia con la clave dada y llame al Fernet.encrypt()
o Fernet.decrypt()
, tanto el mensaje de texto sin formato para cifrar como el token cifrado son bytes
objetos.
encrypt()
y las decrypt()
funciones se verían así:
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)
Manifestación:
>>> 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 con contraseña - clave derivada de contraseña, debilita un poco la seguridad
Puede utilizar una contraseña en lugar de una clave secreta, siempre que utilice un método de derivación de claves seguro . Luego, debe incluir la sal y el recuento de iteraciones HMAC en el mensaje, por lo que el valor cifrado ya no es compatible con Fernet sin separar primero sal, recuento y token de 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)
Manifestación:
>>> message = 'John Doe'
>>> password = 'mypass'
>>> password_encrypt(message.encode(), password)
b'9Ljs-w8IRM3XT1NDBbSBuQABhqCAAAAAAFyJdhiCPXms2vQHO7o81xZJn5r8_PAtro8Qpw48kdKrq4vt-551BCUbcErb_GyYRz8SVsu8hxTXvvKOn9QdewRGDfwx'
>>> token = _
>>> password_decrypt(token, password).decode()
'John Doe'
La inclusión de la sal en la salida permite utilizar un valor de sal aleatorio, lo que a su vez garantiza que la salida cifrada sea totalmente aleatoria independientemente de la reutilización de la contraseña o la repetición de mensajes. La inclusión del recuento de iteraciones garantiza que pueda ajustar los aumentos del rendimiento de la CPU con el tiempo sin perder la capacidad de descifrar mensajes más antiguos.
Una contraseña sola puede ser tan segura como una clave aleatoria Fernet de 32 bytes, siempre que genere una contraseña aleatoria adecuada a partir de un grupo de tamaño similar. 32 bytes le dan 256 ^ 32 números de claves, por lo que si usa un alfabeto de 74 caracteres (26 superiores, 26 inferiores, 10 dígitos y 12 símbolos posibles), entonces su contraseña debe tener al menos math.ceil(math.log(256 ** 32, 74))
== 42 caracteres. Sin embargo, una mayor cantidad de iteraciones de HMAC bien seleccionadas puede mitigar un poco la falta de entropía, ya que esto hace que sea mucho más costoso para un atacante ingresar por la fuerza bruta.
Solo sepa que elegir una contraseña más corta pero razonablemente segura no paralizará este esquema, solo reduce el número de valores posibles que un atacante de fuerza bruta tendría que buscar; asegúrese de elegir una contraseña lo suficientemente segura para sus requisitos de seguridad .
Alternativas
Oscureciendo
Una alternativa es no cifrar . No se sienta tentado a usar un cifrado de baja seguridad o una implementación casera de, digamos Vignere. No hay seguridad en estos enfoques, pero pueden darle a un desarrollador sin experiencia que se le encomiende la tarea de mantener su código en el futuro la ilusión de seguridad, que es peor que ninguna seguridad.
Si todo lo que necesita es oscuridad, simplemente base64 los datos; para los requisitos de seguridad URL, la base64.urlsafe_b64encode()
función está bien. No use una contraseña aquí, solo codifíquela y listo. Como máximo, agregue algo de compresión (como 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))
Esto se convierte b'Hello world!'
en b'eNrzSM3JyVcozy_KSVEEAB0JBF4='
.
Integridad solamente
Si todo lo que necesita es una forma de asegurarse de que se puede confiar en que los datos no se modificarán después de haber sido enviados a un cliente que no son de confianza y recibidos de vuelta, entonces desea firmar los datos, puede usar la hmac
biblioteca para esto con SHA1 (aún considerado seguro para la firma HMAC ) o mejor:
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)
Use esto para firmar datos, luego adjunte la firma con los datos y envíelo al cliente. Cuando reciba los datos, divida los datos y la firma y verifique. Establecí el algoritmo predeterminado en SHA256, por lo que necesitará una clave de 32 bytes:
key = secrets.token_bytes(32)
Es posible que desee ver la itsdangerous
biblioteca , que incluye todo esto con serialización y deserialización en varios formatos.
Usar cifrado AES-GCM para proporcionar cifrado e integridad
Fernet se basa en AEC-CBC con una firma HMAC para garantizar la integridad de los datos cifrados; un atacante malintencionado no puede alimentar su sistema con datos sin sentido para mantener su servicio ocupado funcionando en círculos con una entrada incorrecta, porque el texto cifrado está firmado.
El cifrado de bloque en modo Galois / Counter produce texto cifrado y una etiqueta para el mismo propósito, por lo que puede usarse para los mismos propósitos. La desventaja es que, a diferencia de Fernet, no existe una receta única y fácil de usar para reutilizar en otras plataformas. AES-GCM tampoco usa relleno, por lo que este texto cifrado de cifrado coincide con la longitud del mensaje de entrada (mientras que Fernet / AES-CBC cifra los mensajes en bloques de longitud fija, oscureciendo un poco la longitud del mensaje).
AES256-GCM toma el secreto habitual de 32 bytes como clave:
key = secrets.token_bytes(32)
luego usa
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()
He incluido una marca de tiempo para admitir los mismos casos de uso de tiempo de vida que admite Fernet.
Otros enfoques en esta página, en Python 3
AES CFB - como CBC pero sin la necesidad de rellenar
Este es el enfoque que sigue All Іѕ Vаиітy , aunque incorrectamente. Esta es la cryptography
versión, pero tenga en cuenta que incluyo el IV en el texto cifrado , no debe almacenarse como global (reutilizar un IV debilita la seguridad de la clave y almacenarlo como un módulo global significa que se volverá a generar la próxima invocación de Python, haciendo que todo el texto cifrado no se pueda cifrar):
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()
Esto carece del blindaje adicional de una firma HMAC y no hay marca de tiempo; tendrías que agregarlos tú mismo.
Lo anterior también ilustra lo fácil que es combinar incorrectamente los bloques de construcción de criptografía básica; Todo el manejo incorrecto de Vаиітy del valor de IV puede conducir a una violación de datos o que todos los mensajes encriptados sean ilegibles porque el IV se pierde. En cambio, usar Fernet lo protege de tales errores.
AES ECB - no seguro
Si implementó anteriormente el cifrado AES ECB y aún necesita admitirlo en Python 3, también puede hacerlo con cryptography
. Se aplican las mismas advertencias, ECB no es lo suficientemente seguro para aplicaciones de la vida real . Reimplementando esa respuesta para Python 3, agregando manejo automático de relleno:
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()
Nuevamente, esto carece de la firma HMAC, y no debería usar ECB de todos modos. Lo anterior está solo para ilustrar que cryptography
puede manejar los bloques de construcción criptográficos comunes, incluso los que realmente no debería usar.