182 votes

Comment encoder une chaîne de caractères en fonction d'un mot de passe ?

Python dispose-t-il d'un moyen simple et intégré d'encoder/décoder des chaînes de caractères à l'aide d'un mot de passe ?

Algo así:

>>> encode('John Doe', password = 'mypass')
'sjkl28cn2sx0'
>>> decode('sjkl28cn2sx0', password = 'mypass')
'John Doe'

Ainsi, la chaîne "John Doe" est cryptée sous la forme "sjkl28cn2sx0". Pour obtenir la chaîne originale, je dois la "déverrouiller" avec la clé "mypass", qui est un mot de passe dans mon code source. J'aimerais que ce soit la façon dont je peux crypter/décrypter un document Word avec un mot de passe.

J'aimerais utiliser ces chaînes cryptées comme paramètres d'URL. Mon objectif est l'obscurcissement, pas une sécurité forte ; rien de critique n'est codé. Je sais que je pourrais utiliser une table de base de données pour stocker les clés et les valeurs, mais j'essaie d'être minimaliste.

202voto

Martijn Pieters Points 271458

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.

83voto

smehmood Points 919

En supposant que vous soyez seulement à la recherche d'un simple obscurcissement qui masquera les choses à la muy et vous ne cherchez pas à utiliser des bibliothèques tierces. Je recommanderais quelque chose comme le chiffrement Vigenere. C'est l'un des chiffres anciens les plus puissants.

Chiffre de Vigenère

C'est rapide et facile à mettre en œuvre. Quelque chose comme :

import base64

def encode(key, string):
    encoded_chars = []
    for i in xrange(len(string)):
        key_c = key[i % len(key)]
        encoded_c = chr(ord(string[i]) + ord(key_c) % 256)
        encoded_chars.append(encoded_c)
    encoded_string = "".join(encoded_chars)
    return base64.urlsafe_b64encode(encoded_string)

Le décodage est à peu près le même, sauf que l'on soustrait la clé.

Il est beaucoup plus difficile à casser si les chaînes que vous encodez sont courtes et/ou s'il est difficile de deviner la longueur de la phrase d'authentification utilisée.

Si vous cherchez quelque chose de cryptographique, PyCrypto est probablement votre meilleur choix, bien que les réponses précédentes négligent certains détails : Le mode ECB de PyCrypto exige que votre message ait une longueur multiple de 16 caractères. Vous devez donc remplir votre message. De plus, si vous voulez les utiliser comme paramètres d'URL, utilisez base64.urlsafe_b64_encode() au lieu de la version standard. Il remplace quelques caractères de l'alphabet base64 par des caractères URL-safe (comme son nom l'indique).

Cependant, vous devez être ABSOLUMENT certain que cette muy une fine couche d'obscurcissement suffit à vos besoins avant de l'utiliser. L'article de Wikipedia que j'ai cité fournit des instructions détaillées pour casser le cryptogramme, de sorte que n'importe qui, avec un peu de détermination, peut facilement le casser.

76voto

Will Points 30630

Comme vous déclarez explicitement que vous voulez de l'obscurité et non de la sécurité, nous éviterons de vous réprimander pour la faiblesse de ce que vous suggérez :)

Ainsi, en utilisant PyCrypto :

import base64
from Crypto.Cipher import AES

msg_text = b'test some plain text here'.rjust(32)
secret_key = b'1234567890123456'

cipher = AES.new(secret_key,AES.MODE_ECB) # never use ECB in strong systems obviously
encoded = base64.b64encode(cipher.encrypt(msg_text))
print(encoded)
decoded = cipher.decrypt(base64.b64decode(encoded))
print(decoded)

Si quelqu'un met la main sur votre base de données et votre base de code, il pourra décoder les données cryptées. Conservez vos secret_key sûr !

62voto

Reppo-Master7 Points 89

Voici une version Python 3 des fonctions de @qneill 's répondre :

import base64
def encode(key, clear):
    enc = []
    for i in range(len(clear)):
        key_c = key[i % len(key)]
        enc_c = chr((ord(clear[i]) + ord(key_c)) % 256)
        enc.append(enc_c)
    return base64.urlsafe_b64encode("".join(enc).encode()).decode()

def decode(key, enc):
    dec = []
    enc = base64.urlsafe_b64decode(enc).decode()
    for i in range(len(enc)):
        key_c = key[i % len(key)]
        dec_c = chr((256 + ord(enc[i]) - ord(key_c)) % 256)
        dec.append(dec_c)
    return "".join(dec)

Les encodages/décodages supplémentaires sont nécessaires parce que Python 3 a divisé les chaînes de caractères et les tableaux d'octets en deux concepts différents, et a mis à jour ses API pour refléter cela.

57voto

qneill Points 347

Le "encoded_c" mentionné dans l'article de @smehmood Réponse au cryptogramme de Vigenere devrait être "key_c".

Voici les fonctions de codage/décodage qui fonctionnent.

import base64
def encode(key, clear):
    enc = []
    for i in range(len(clear)):
        key_c = key[i % len(key)]
        enc_c = chr((ord(clear[i]) + ord(key_c)) % 256)
        enc.append(enc_c)
    return base64.urlsafe_b64encode("".join(enc))

def decode(key, enc):
    dec = []
    enc = base64.urlsafe_b64decode(enc)
    for i in range(len(enc)):
        key_c = key[i % len(key)]
        dec_c = chr((256 + ord(enc[i]) - ord(key_c)) % 256)
        dec.append(dec_c)
    return "".join(dec)

Avis de non-responsabilité : Comme l'indiquent les commentaires, cette ne doit pas être utilisé pour protéger les données dans une application réelle, à moins que vous ne lisiez ceci et que cela ne vous dérange pas de parler avec des avocats :

Quel est le problème avec le cryptage XOR ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X