Python n'a pas de schémas de chiffrement intégrés, non. Vous devez également prendre au sérieux le stockage de données cryptées; des schémas de cryptage triviaux qu'un développeur considère comme non sécurisés et un schéma de jouet peut très bien être confondu avec un schéma sécurisé par un développeur moins expérimenté. Si vous cryptez, cryptez correctement.
Cependant, vous n'avez pas besoin de beaucoup de travail pour implémenter un schéma de cryptage approprié. Tout d'abord, ne réinventez pas la roue de cryptographie , utilisez une bibliothèque de cryptographie de confiance pour gérer cela pour vous. Pour Python 3, cette bibliothèque de confiance est cryptography
.
Je recommande également que le cryptage et le décryptage s'appliquent aux octets ; encoder d'abord les messages texte en octets; stringvalue.encode()
encode en UTF8, facilement rétabli en utilisant bytesvalue.decode()
.
Enfin, lors du cryptage et du décryptage, nous parlons de clés , pas de mots de passe. Une clé ne doit pas être mémorable par un humain, c'est quelque chose que vous stockez dans un endroit secret mais lisible par machine, alors qu'un mot de passe peut souvent être lisible et mémorisé par l'homme. Vous pouvez dériver une clé d'un mot de passe, avec un peu de soin.
Mais pour une application Web ou un processus s'exécutant dans un cluster sans qu'une attention humaine ne soit nécessaire pour continuer à l'exécuter, vous souhaitez utiliser une clé. Les mots de passe sont utilisés lorsque seul un utilisateur final a besoin d'accéder aux informations spécifiques. Même dans ce cas, vous sécurisez généralement l'application avec un mot de passe, puis échangez des informations cryptées à l'aide d'une clé, peut-être attachée au compte utilisateur.
Chiffrement à clé symétrique
Fernet - AES CBC + HMAC, fortement recommandé
La cryptography
bibliothèque comprend la recette Fernet , une recette des meilleures pratiques pour l'utilisation de la cryptographie. Fernet est un standard ouvert , avec des implémentations prêtes dans un large éventail de langages de programmation et il intègre le cryptage AES CBC pour vous avec des informations de version, un horodatage et une signature HMAC pour empêcher la falsification des messages.
Fernet facilite le chiffrement et le déchiffrement des messages et vous protège. C'est la méthode idéale pour crypter des données avec un secret.
Je vous recommande d'utiliser Fernet.generate_key()
pour générer une clé sécurisée. Vous pouvez également utiliser un mot de passe (section suivante), mais une clé secrète complète de 32 octets (16 octets pour chiffrer avec, plus 16 autres pour la signature) sera plus sécurisée que la plupart des mots de passe auxquels vous pourriez penser.
La clé que Fernet génère est un bytes
objet avec des caractères base64 URL et fichier sûr, donc imprimable:
from cryptography.fernet import Fernet
key = Fernet.generate_key() # store in a secure location
print("Key:", key.decode())
Pour crypter ou décrypter des messages, créez une Fernet()
instance avec la clé donnée et appelez Fernet.encrypt()
ou Fernet.decrypt()
, le message en clair à crypter et le jeton crypté sont des bytes
objets.
encrypt()
et les decrypt()
fonctions ressembleraient à:
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)
Démo:
>>> 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 avec mot de passe - clé dérivée du mot de passe, affaiblit quelque peu la sécurité
Vous pouvez utiliser un mot de passe au lieu d'une clé secrète, à condition d' utiliser une méthode de dérivation de clé forte . Vous devez ensuite inclure le salt et le nombre d'itérations HMAC dans le message, de sorte que la valeur chiffrée n'est plus compatible avec Fernet sans d'abord séparer salt, count et Fernet token:
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)
Démo:
>>> message = 'John Doe'
>>> password = 'mypass'
>>> password_encrypt(message.encode(), password)
b'9Ljs-w8IRM3XT1NDBbSBuQABhqCAAAAAAFyJdhiCPXms2vQHO7o81xZJn5r8_PAtro8Qpw48kdKrq4vt-551BCUbcErb_GyYRz8SVsu8hxTXvvKOn9QdewRGDfwx'
>>> token = _
>>> password_decrypt(token, password).decode()
'John Doe'
L'inclusion du sel dans la sortie permet d'utiliser une valeur de sel aléatoire, ce qui garantit à son tour que la sortie chiffrée est entièrement aléatoire, quelle que soit la réutilisation du mot de passe ou la répétition du message. L'inclusion du nombre d'itérations garantit que vous pouvez ajuster les performances du processeur au fil du temps sans perdre la capacité de déchiffrer les anciens messages.
Un mot de passe seul peut être aussi sûr qu'une clé aléatoire Fernet de 32 octets, à condition de générer un mot de passe correctement aléatoire à partir d'un pool de taille similaire. 32 octets vous donne un nombre de 256 ^ 32 touches, donc si vous utilisez un alphabet de 74 caractères (26 majuscules, 26 minuscules, 10 chiffres et 12 symboles possibles), votre mot de passe doit comporter au moins math.ceil(math.log(256 ** 32, 74))
== 42 caractères. Cependant, un plus grand nombre d'itérations HMAC bien sélectionnées peut atténuer quelque peu le manque d'entropie, car cela rend beaucoup plus coûteux pour un attaquant de se frayer un chemin.
Sachez simplement que le choix d'un mot de passe plus court mais toujours raisonnablement sécurisé ne paralysera pas ce schéma, il réduit simplement le nombre de valeurs possibles qu'un attaquant par force brute devrait rechercher; assurez-vous de choisir un mot de passe suffisamment fort pour vos exigences de sécurité .
Alternatives
Obscurcissant
Une alternative est de ne pas crypter . Ne soyez pas tenté d'utiliser simplement un chiffrement à faible sécurité ou une implémentation à domicile de, disons Vignere. Il n'y a pas de sécurité dans ces approches, mais peuvent donner à un développeur inexpérimenté qui a la tâche de maintenir votre code à l'avenir l'illusion de la sécurité, ce qui est pire que pas de sécurité du tout.
Si tout ce dont vous avez besoin est d'obscurité, base64 les données; pour les exigences de sécurité URL, la base64.urlsafe_b64encode()
fonction est correcte. N'utilisez pas de mot de passe ici, encodez simplement et vous avez terminé. Tout au plus, ajoutez un peu de compression (comme 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))
Cela se transforme b'Hello world!'
en b'eNrzSM3JyVcozy_KSVEEAB0JBF4='
.
Intégrité uniquement
Si tout ce dont vous avez besoin est un moyen de vous assurer que les données peuvent être fiables pour ne pas être modifiées après avoir été envoyées à un client non approuvé et reçues en retour, alors vous voulez signer les données, vous pouvez utiliser la hmac
bibliothèque pour cela avec SHA1 (toujours considéré comme sécurisé pour la signature HMAC ) ou mieux:
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)
Utilisez-le pour signer les données, puis attachez la signature avec les données et envoyez-la au client. Lorsque vous recevez les données, divisez les données et la signature et vérifiez. J'ai défini l'algorithme par défaut sur SHA256, vous aurez donc besoin d'une clé de 32 octets:
key = secrets.token_bytes(32)
Vous voudrez peut-être regarder la itsdangerous
bibliothèque , qui contient tout cela avec la sérialisation et la désérialisation dans divers formats.
Utilisation du cryptage AES-GCM pour assurer le cryptage et l'intégrité
Fernet s'appuie sur AEC-CBC avec une signature HMAC pour assurer l'intégrité des données cryptées; un attaquant malveillant ne peut pas nourrir votre système de données absurdes pour garder votre service occupé à tourner en rond avec une mauvaise entrée, car le texte chiffré est signé.
Le chiffrement par blocs en mode Galois / Counter produit un texte chiffré et une étiquette pour servir le même but, donc peut être utilisé pour servir les mêmes buts. L'inconvénient est que, contrairement à Fernet, il n'y a pas de recette universelle facile à utiliser à réutiliser sur d'autres plates-formes. AES-GCM n'utilise pas non plus de remplissage, donc ce texte chiffré de chiffrement correspond à la longueur du message d'entrée (alors que Fernet / AES-CBC crypte les messages en blocs de longueur fixe, obscurcissant quelque peu la longueur du message).
AES256-GCM prend le secret habituel de 32 octets comme clé:
key = secrets.token_bytes(32)
puis utilisez
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()
J'ai inclus un horodatage pour prendre en charge les mêmes cas d'utilisation de durée de vie que Fernet prend en charge.
Autres approches sur cette page, en Python 3
AES CFB - comme CBC mais sans besoin de pad
C'est l'approche que suit All Іѕ Vаиітy , quoique incorrectement. Ceci est la cryptography
version, mais notez que j'inclus l'IV dans le texte chiffré , il ne doit pas être stocké en tant que global (la réutilisation d'un IV affaiblit la sécurité de la clé, et le stocker en tant que module global signifie qu'il sera re-généré la prochaine invocation Python, rendant tout le texte chiffré indéchiffrable):
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()
Il n'y a pas de blindage supplémentaire d'une signature HMAC et il n'y a pas d'horodatage; vous devrez les ajouter vous-même.
Ce qui précède illustre également à quel point il est facile de combiner incorrectement les blocs de base de la cryptographie; Toute manipulation incorrecte de la valeur IV par Vаиітy peut entraîner une violation de données ou tous les messages cryptés sont illisibles parce que l'IV est perdu. L'utilisation de Fernet vous protège à la place de telles erreurs.
AES ECB - non sécurisé
Si vous avez précédemment implémenté le chiffrement AES ECB et que vous devez toujours le prendre en charge dans Python 3, vous pouvez toujours le faire avec cryptography
. Les mêmes mises en garde s'appliquent, ECB n'est pas suffisamment sécurisé pour les applications réelles . Ré-implémenter cette réponse pour Python 3, en ajoutant la gestion automatique du remplissage:
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()
Encore une fois, il manque la signature HMAC et vous ne devriez pas utiliser ECB de toute façon. Ce qui précède n'est là que pour illustrer ce qui cryptography
peut gérer les blocs de construction cryptographiques courants, même ceux que vous ne devriez pas utiliser.