Python non ha schemi di crittografia incorporati, no. Dovresti anche prendere sul serio l'archiviazione dei dati crittografati; schemi di crittografia banali che uno sviluppatore considera insicuri e uno schema giocattolo potrebbe essere scambiato per uno schema sicuro da uno sviluppatore meno esperto. Se crittografa, crittografa correttamente.
Tuttavia, non è necessario lavorare molto per implementare uno schema di crittografia adeguato. Prima di tutto, non reinventare la ruota crittografica , usa una libreria di crittografia affidabile per gestirlo per te. Per Python 3, quella libreria affidabile ècryptography
.
Raccomando anche che la crittografia e la decrittografia si applichino ai byte ; codificare prima i messaggi di testo in byte; stringvalue.encode()
codifica in UTF8, facilmente ripristinato di nuovo utilizzando bytesvalue.decode()
.
Ultimo ma non meno importante, quando si crittografa e decrittografa, si parla di chiavi , non di password. Una chiave non dovrebbe essere memorabile dall'uomo, è qualcosa che si memorizza in un luogo segreto ma leggibile dalla macchina, mentre una password spesso può essere leggibile e memorizzata dall'uomo. È possibile ricavare una chiave da una password, con un po 'di attenzione.
Ma per un'applicazione Web o un processo in esecuzione in un cluster senza l'attenzione umana per continuare a eseguirlo, è necessario utilizzare una chiave. Le password servono quando solo un utente finale ha bisogno di accedere alle informazioni specifiche. Anche in questo caso, di solito proteggi l'applicazione con una password, quindi scambi le informazioni crittografate utilizzando una chiave, forse una allegata all'account utente.
Crittografia a chiave simmetrica
Fernet - AES CBC + HMAC, fortemente raccomandato
La cryptography
libreria include la ricetta Fernet , una ricetta delle migliori pratiche per l'utilizzo della crittografia. Fernet è uno standard aperto , con implementazioni pronte in un'ampia gamma di linguaggi di programmazione e racchiude la crittografia AES CBC per te con informazioni sulla versione, un timestamp e una firma HMAC per prevenire la manomissione dei messaggi.
Fernet rende molto facile crittografare e decrittografare i messaggi e tenerti al sicuro. È il metodo ideale per crittografare i dati con un segreto.
Ti consiglio di usare Fernet.generate_key()
per generare una chiave sicura. Puoi anche usare una password (sezione successiva), ma una chiave segreta a 32 byte completa (16 byte con cui crittografare, più altri 16 per la firma) sarà più sicura della maggior parte delle password a cui potresti pensare.
La chiave che Fernet genera è un bytes
oggetto con caratteri base64 URL e file safe, quindi stampabile:
from cryptography.fernet import Fernet
key = Fernet.generate_key() # store in a secure location
print("Key:", key.decode())
Per crittografare o decrittografare i messaggi, creare Fernet()
un'istanza con la chiave fornita e chiamare Fernet.encrypt()
oFernet.decrypt()
, sia il messaggio di testo normale da crittografare che il token crittografato sono bytes
oggetti.
encrypt()
e le decrypt()
funzioni sarebbero simili:
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)
demo:
>>> 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 password - chiave derivata dalla password, indebolisce un po 'la sicurezza
È possibile utilizzare una password invece di una chiave segreta, a condizione di utilizzare un metodo di derivazione della chiave efficace . È quindi necessario includere il sale e il conteggio delle iterazioni HMAC nel messaggio, quindi il valore crittografato non è più compatibile con Fernet senza prima separare salt, count e 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)
demo:
>>> message = 'John Doe'
>>> password = 'mypass'
>>> password_encrypt(message.encode(), password)
b'9Ljs-w8IRM3XT1NDBbSBuQABhqCAAAAAAFyJdhiCPXms2vQHO7o81xZJn5r8_PAtro8Qpw48kdKrq4vt-551BCUbcErb_GyYRz8SVsu8hxTXvvKOn9QdewRGDfwx'
>>> token = _
>>> password_decrypt(token, password).decode()
'John Doe'
Includere il salt nell'output rende possibile utilizzare un valore salt casuale, che a sua volta garantisce che l'output crittografato sia completamente casuale indipendentemente dal riutilizzo della password o dalla ripetizione del messaggio. L'inclusione del conteggio delle iterazioni garantisce la possibilità di regolare gli aumenti delle prestazioni della CPU nel tempo senza perdere la capacità di decrittografare i messaggi meno recenti.
Una password da sola può essere sicura come una chiave casuale Fernet a 32 byte, a patto di generare una password adeguatamente casuale da un pool di dimensioni simili. 32 byte fornisce un numero di chiavi di 256 ^ 32, quindi se utilizzi un alfabeto di 74 caratteri (26 in alto, 26 in basso, 10 cifre e 12 possibili simboli), la tua password dovrebbe essere math.ceil(math.log(256 ** 32, 74))
lunga almeno == 42 caratteri. Tuttavia, a ben selezionato di iterazioni HMAC può mitigare in qualche modo la mancanza di entropia poiché ciò rende molto più costoso per un attaccante penetrare con la forza bruta.
Sappi solo che la scelta di una password più corta ma comunque ragionevolmente sicura non paralizzerà questo schema, ma ridurrà semplicemente il numero di possibili valori che un aggressore di forza bruta dovrebbe cercare; assicurati di scegliere una password sufficientemente complessa per i tuoi requisiti di sicurezza .
alternative
oscuramento
Un'alternativa è non crittografare . Non essere tentato di utilizzare solo un codice a bassa sicurezza o un'implementazione casalinga di, ad esempio Vignere. Non c'è sicurezza in questi approcci, ma può dare a uno sviluppatore inesperto a cui è affidato il compito di mantenere il codice in futuro l'illusione della sicurezza, che è peggio di nessuna sicurezza.
Se tutto ciò di cui hai bisogno è l'oscurità, basare solo i dati; per requisiti sicuri per l'URL, la base64.urlsafe_b64encode()
funzione va bene. Non utilizzare una password qui, codifica e il gioco è fatto. Al massimo, aggiungi un po 'di compressione (come 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))
Questo si trasforma b'Hello world!'
in b'eNrzSM3JyVcozy_KSVEEAB0JBF4='
.
Solo integrità
Se tutto ciò di cui hai bisogno è un modo per assicurarti che i dati possano essere considerati attendibili per essere inalterati dopo essere stati inviati a un client non attendibile e ricevuti di nuovo, allora vuoi firmare i dati, puoi usare la hmac
libreria per questo con SHA1 (ancora considerato sicuro per la firma HMAC ) o meglio:
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)
Usalo per firmare i dati, quindi allega la firma con i dati e inviala al client. Quando ricevi indietro i dati, dividi i dati e la firma e verifica. Ho impostato l'algoritmo predefinito su SHA256, quindi avrai bisogno di una chiave a 32 byte:
key = secrets.token_bytes(32)
Potresti voler guardare la itsdangerous
libreria , che racchiude tutto questo con serializzazione e deserializzazione in vari formati.
Utilizzo della crittografia AES-GCM per fornire crittografia e integrità
Fernet si basa su AEC-CBC con una firma HMAC per garantire l'integrità dei dati crittografati; un malintenzionato non può fornire al tuo sistema dati senza senso per mantenere il tuo servizio impegnato in esecuzione in circoli con input errato, perché il testo cifrato è firmato.
La crittografia a blocchi in modalità Galois / Counter produce testo cifrato e un tag per avere lo stesso scopo, quindi può essere utilizzato per gli stessi scopi. Lo svantaggio è che, a differenza di Fernet, non esiste una ricetta unica e facile da usare da riutilizzare su altre piattaforme. Inoltre, AES-GCM non utilizza il riempimento, quindi questo testo cifrato corrisponde alla lunghezza del messaggio di input (mentre Fernet / AES-CBC crittografa i messaggi in blocchi di lunghezza fissa, oscurando un po 'la lunghezza del messaggio).
AES256-GCM prende il solito segreto di 32 byte come chiave:
key = secrets.token_bytes(32)
quindi utilizzare
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()
Ho incluso un timestamp per supportare gli stessi casi d'uso time-to-live supportati da Fernet.
Altri approcci in questa pagina, in Python 3
AES CFB - come CBC ma senza bisogno di pad
Questo è l'approccio seguito da All Іs Vаиітy , anche se in modo errato. Questaèla cryptography
versione, ma nota che includo l'IV nel testo cifrato , non dovrebbe essere memorizzato come globale (riutilizzare un IV indebolisce la sicurezza della chiave e memorizzarlo come modulo globale significa che verrà rigenerato la successiva invocazione di Python, che rende tutto il testo cifrato non decodificabile):
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()
Questo manca l'armatura aggiunta di una firma HMAC e non c'è il timestamp; dovresti aggiungerli tu stesso.
Quanto sopra illustra anche quanto sia facile combinare i blocchi di base della crittografia in modo errato; La gestione errata del valore IV da parte di Vаиітy può portare a una violazione dei dati o a tutti i messaggi crittografati illeggibili perché l'IV è perso. Usare Fernet invece ti protegge da tali errori.
AES ECB - non sicuro
Se in precedenza hai implementato la crittografia AES ECB e devi ancora supportarla in Python 3, puoi farlo anche tu cryptography
. Si applicano gli stessi avvertimenti, ECB non è abbastanza sicuro per applicazioni nella vita reale . Reimplementare quella risposta per Python 3, aggiungendo la gestione automatica del 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()
Ancora una volta, questo manca della firma HMAC e comunque non dovresti usare ECB. Quanto sopra è solo per illustrare che è in cryptography
grado di gestire i comuni blocchi crittografici, anche quelli che non dovresti effettivamente usare.