Python n'a pas de système de cryptage intégré, 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 comprend comme étant non sécurisés et un schéma de jouet peuvent très bien être pris pour un schéma sécurisé par un développeur moins expérimenté. Si vous cryptez, cryptez correctement.
Cependant, il n'est pas nécessaire de travailler beaucoup pour mettre en œuvre un système de cryptage adéquat. Tout d'abord, ne pas réinventer la roue de la 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 à octets ; coder les messages textuels en octets d'abord ; stringvalue.encode()
encode en UTF8, ce qui peut être facilement inversé en utilisant bytesvalue.decode()
.
Enfin, lors du cryptage et du décryptage, on parle de clés et non des mots de passe. Une clé ne doit pas être mémorisable par l'homme, c'est quelque chose que vous stockez dans un endroit secret mais qui est lisible par une machine, alors qu'un mot de passe peut souvent être lisible par l'homme et mémorisé. En revanche, un mot de passe peut souvent être lu par l'homme et mémorisé. puede dériver une clé à partir d'un mot de passe, avec un peu d'attention.
Mais pour une application web ou un processus s'exécutant dans un cluster sans attention humaine pour continuer à le faire fonctionner, il est préférable d'utiliser une clé. Les mots de passe sont utilisés lorsque seul un utilisateur final a besoin d'accéder à des informations spécifiques. Même dans ce cas, vous sécurisez généralement l'application à l'aide d'un mot de passe, puis vous échangez des informations cryptées à l'aide d'une clé, éventuellement liée au compte de l'utilisateur.
Cryptage à clé symétrique
Fernet - AES CBC + HMAC, fortement recommandé
En cryptography
comprend la bibliothèque Recette du Fernet une recette des meilleures pratiques pour l'utilisation de la cryptographie. Le Fernet est une norme ouverte , avec des implémentations prêtes à l'emploi dans un large éventail de langages de programmation, et il intègre pour vous le cryptage AES CBC avec des informations sur la version, un horodatage et une signature HMAC pour empêcher la falsification des messages.
Le Fernet permet de crypter et de décrypter très facilement les messages y vous sécuriser. 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 le chiffrement, plus 16 autres pour la signature) sera plus sûre que la plupart des mots de passe auxquels vous pouvez penser.
La clé que génère le Fernet est une bytes
avec des caractères base64 sûrs pour les URL et les fichiers, donc imprimables :
from cryptography.fernet import Fernet
key = Fernet.generate_key() # store in a secure location
# PRINTING FOR DEMO PURPOSES ONLY, don't do this in production code
print("Key:", key.decode())
Pour crypter ou décrypter les messages, créez un fichier Fernet()
avec la clé donnée, et appeler l'instance Fernet.encrypt()
o Fernet.decrypt()
le message en clair à crypter et le jeton crypté sont tous deux bytes
objets.
encrypt()
et decrypt()
se présenterait comme suit :
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émonstration :
>>> key = Fernet.generate_key()
>>> print(key.decode())
GZWKEhHGNopxRdOHS4H4IyKhLQ8lwnyU7vRLrM3sebY=
>>> message = 'John Doe'
>>> token = encrypt(message.encode(), key)
>>> print(token)
'gAAAAABciT3pFbbSihD_HZBZ8kqfAj94UhknamBuirZWKivWOukgKQ03qE2mcuvpuwCSuZ-X_Xkud0uWQLZ5e-aOwLC0Ccnepg=='
>>> 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 de utiliser une méthode de dérivation de clé forte . Vous devez alors inclure le sel et le nombre d'itérations HMAC dans le message, de sorte que la valeur cryptée n'est plus compatible avec le système Fernet sans séparation préalable du sel, du nombre d'itérations et du jeton 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)
Démonstration :
>>> 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 le résultat permet d'utiliser une valeur de sel aléatoire, ce qui garantit que le résultat chiffré est totalement aléatoire, indépendamment de la réutilisation du mot de passe ou de la répétition du message. L'inclusion du nombre d'itérations permet de s'adapter à l'augmentation des performances de l'unité centrale au fil du temps sans perdre la possibilité de déchiffrer les messages plus anciens.
Un mot de passe seul puede est aussi sûr qu'une clé aléatoire Fernet de 32 octets, à condition que vous génériez un mot de passe correctement aléatoire à partir d'un ensemble de taille similaire. 32 octets donnent un nombre de clés de 256 ^ 32, 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 être d'au moins math.ceil(math.log(256 ** 32, 74))
== 42 caractères. Cependant, un un plus grand nombre d'itérations HMAC bien choisies peut atténuer quelque peu le manque d'entropie, car il est beaucoup plus coûteux pour un attaquant de forcer brutalement son entrée.
Sachez que le choix d'un mot de passe plus court mais toujours raisonnablement sûr ne paralysera pas ce système, il réduira simplement le nombre de valeurs possibles qu'un attaquant par force brute devra rechercher. un mot de passe suffisamment fort pour répondre à vos exigences de sécurité .
Alternatives
Obscurcissement
Une alternative est ne pas crypter . Ne vous laissez pas tenter par l'utilisation d'un algorithme de chiffrement à faible sécurité ou d'une implémentation artisanale de Vignere, par exemple. Ces approches n'offrent aucune sécurité, mais peuvent donner à un développeur inexpérimenté chargé de maintenir votre code à l'avenir l'illusion de la sécurité, ce qui est pire que l'absence de sécurité.
Si tout ce dont vous avez besoin est l'obscurité, il suffit de baser64 les données ; pour les exigences de sécurité des URL, l'option base64.urlsafe_b64encode()
fonction est très bien. 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 traduit par b'Hello world!'
en b'eNrzSM3JyVcozy_KSVEEAB0JBF4='
.
Intégrité uniquement
Si tout ce dont vous avez besoin, c'est d'un moyen de vous assurer que les données sont fiables. inaltéré après avoir été envoyées à un client non fiable et reçues en retour, et que vous souhaitez signer les données, vous pouvez utiliser la directive hmac
bibliothèque pour cela avec SHA1 (toujours considéré comme sûr 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 des données, puis joignez la signature aux données et envoyez le tout au client. Lorsque vous recevez les données en retour, séparez les données et la signature et vérifiez. L'algorithme par défaut est SHA256, vous aurez donc besoin d'une clé de 32 octets :
key = secrets.token_bytes(32)
Vous pouvez consulter le site itsdangerous
bibliothèque qui regroupe tout cela avec la sérialisation et la désérialisation dans différents 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 garantir l'intégrité des données chiffrées ; un attaquant malveillant ne peut pas alimenter votre système en données absurdes pour que votre service tourne en rond avec de mauvaises entrées, parce que le texte chiffré est signé.
En Chiffrement par blocs de Galois / mode compteur produit un texte chiffré et un étiquette pour servir le même objectif, ils peuvent donc être utilisés pour servir les mêmes objectifs. L'inconvénient est que, contrairement au Fernet, il n'existe pas de recette unique facile à utiliser et à réutiliser sur d'autres plates-formes. AES-GCM n'utilise pas non plus de rembourrage, de sorte que le texte chiffré correspond à la longueur du message d'entrée (alors que Fernet / AES-CBC chiffre les messages en blocs de longueur fixe, ce qui masque quelque peu la longueur du message).
AES256-GCM utilise comme clé le secret habituel de 32 octets :
key = secrets.token_bytes(32)
puis utiliser
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 la durée de vie que le Fernet.
Autres approches sur cette page, en Python 3
AES CFB - comme le CBC, mais sans avoir besoin de remplir
C'est l'approche qui Tous les Іѕ Vаиітy suit, même si c'est à tort. Il s'agit de la cryptography
mais notez que j'ai inclure 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é lors de la prochaine invocation de 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()
Cette signature n'est pas renforcée par une signature HMAC et il n'y a pas d'horodatage ; vous devez l'ajouter vous-même.
Ce qui précède illustre également à quel point il est facile de combiner incorrectement des éléments de base de la cryptographie. La mauvaise gestion de la valeur IV par All Іѕ Vаиітy peut entraîner une violation des données ou rendre tous les messages cryptés illisibles en raison de la perte de la valeur IV. L'utilisation du Fernet vous met à l'abri de telles erreurs.
AES BCE - pas sûr
Si vous avez déjà mis en œuvre Cryptage AES de la BCE et que vous avez besoin de le supporter dans Python 3, vous pouvez toujours le faire avec cryptography
aussi. Les mêmes réserves s'appliquent, la BCE est pas assez sûr pour les applications réelles . Réimplémentation de cette réponse pour Python 3, en ajoutant la gestion automatique du padding :
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()
Là encore, il manque la signature HMAC, et vous ne devriez pas utiliser ECB de toute façon. Ce qui précède est simplement là pour illustrer le fait que cryptography
peut prendre en charge les blocs de construction cryptographiques courants, même ceux que vous ne devriez pas utiliser.