Python nie ma wbudowanych schematów szyfrowania, nie. Powinieneś również poważnie potraktować przechowywanie zaszyfrowanych danych; trywialne schematy szyfrowania, które jeden programista uważa za niepewne, a schemat zabawkowy może być mylony z bezpiecznym schematem przez mniej doświadczonego programistę. Jeśli szyfrujesz, zaszyfruj poprawnie.
Jednak nie musisz wykonywać wiele pracy, aby zaimplementować odpowiedni schemat szyfrowania. Przede wszystkim nie wymyślaj ponownie koła kryptograficznego , użyj zaufanej biblioteki kryptograficznej, aby załatwić to za Ciebie. W przypadku Pythona 3 ta zaufana biblioteka to cryptography
.
Zalecam również, aby szyfrowanie i deszyfrowanie dotyczyło bajtów ; najpierw zakoduj wiadomości tekstowe do bajtów; stringvalue.encode()
koduje do UTF8, łatwo przywracany ponownie przy użyciu bytesvalue.decode()
.
Wreszcie, podczas szyfrowania i odszyfrowywania mówimy o kluczach , a nie hasłach. Klucz nie powinien być niezapomniany dla człowieka, jest to coś, co przechowujesz w tajnym miejscu, ale można je odczytać maszynowo, podczas gdy hasło często może być czytelne dla człowieka i zapamiętane. Państwo może czerpać klucz z hasłem, przy odrobinie opieki.
Ale aby aplikacja internetowa lub proces działały w klastrze bez ludzkiej uwagi, aby go nadal uruchamiać, chcesz użyć klucza. Hasła są używane, gdy tylko użytkownik końcowy potrzebuje dostępu do określonych informacji. Nawet wtedy zazwyczaj zabezpieczasz aplikację hasłem, a następnie wymieniasz zaszyfrowane informacje za pomocą klucza, być może dołączonego do konta użytkownika.
Szyfrowanie klucza symetrycznego
Fernet - AES CBC + HMAC, zdecydowanie zalecane
cryptography
Biblioteka zawiera przepis Fernet , najlepszy przepis na praktykach za pomocą kryptografii. Fernet to otwarty standard , z gotowymi implementacjami w szerokiej gamie języków programowania, który zawiera szyfrowanie AES CBC wraz z informacjami o wersji, sygnaturą czasową i podpisem HMAC, aby zapobiec manipulowaniu wiadomościami.
Fernet bardzo ułatwia szyfrowanie i odszyfrowywanie wiadomości oraz zapewnia bezpieczeństwo. Jest to idealna metoda szyfrowania danych za pomocą sekretu.
Zalecam użycie Fernet.generate_key()
do wygenerowania bezpiecznego klucza. Możesz też użyć hasła (następna sekcja), ale pełny 32-bajtowy tajny klucz (16 bajtów do zaszyfrowania plus kolejne 16 do podpisu) będzie bezpieczniejszy niż większość haseł, jakie możesz sobie wyobrazić.
Klucz, który generuje Fernet, to bytes
obiekt z adresem URL i plikami bezpiecznymi znakami base64, więc można go wydrukować:
from cryptography.fernet import Fernet
key = Fernet.generate_key() # store in a secure location
print("Key:", key.decode())
Aby zaszyfrować lub odszyfrować wiadomości, utwórz Fernet()
wystąpienie z podanym kluczem i wywołaj Fernet.encrypt()
lub Fernet.decrypt()
, zarówno wiadomość w postaci zwykłego tekstu do zaszyfrowania, jak i zaszyfrowany token są bytes
obiektami.
encrypt()
a decrypt()
funkcje wyglądałyby następująco:
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)
Próbny:
>>> 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 z hasłem - klucz pochodzący z hasła, nieco osłabia bezpieczeństwo
Możesz użyć hasła zamiast tajnego klucza, pod warunkiem, że używasz silnej metody wyprowadzania klucza . Następnie musisz uwzględnić sól i liczbę iteracji HMAC w wiadomości, więc zaszyfrowana wartość nie jest już zgodna z Fernetem bez uprzedniego oddzielenia soli, liczby i tokenu Ferneta:
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)
Próbny:
>>> message = 'John Doe'
>>> password = 'mypass'
>>> password_encrypt(message.encode(), password)
b'9Ljs-w8IRM3XT1NDBbSBuQABhqCAAAAAAFyJdhiCPXms2vQHO7o81xZJn5r8_PAtro8Qpw48kdKrq4vt-551BCUbcErb_GyYRz8SVsu8hxTXvvKOn9QdewRGDfwx'
>>> token = _
>>> password_decrypt(token, password).decode()
'John Doe'
Dołączenie soli do danych wyjściowych umożliwia użycie losowej wartości soli, co z kolei gwarantuje, że zaszyfrowane dane wyjściowe będą w pełni losowe, niezależnie od ponownego użycia hasła lub powtórzenia wiadomości. Uwzględnienie liczby iteracji zapewnia, że możesz dostosować się do wzrostu wydajności procesora w czasie bez utraty możliwości odszyfrowania starszych wiadomości.
Samo hasło może być równie bezpieczne, jak 32-bajtowy losowy klucz Fernet, pod warunkiem że wygenerujesz odpowiednio losowe hasło z puli o podobnej wielkości. 32 bajty to 256 ^ 32 liczby kluczy, więc jeśli używasz alfabetu składającego się z 74 znaków (26 górnych, 26 dolnych, 10 cyfr i 12 możliwych symboli), to Twoje hasło powinno mieć co najmniej math.ceil(math.log(256 ** 32, 74))
== 42 znaki. Jednak dobrze dobrana większa liczba iteracji HMAC może nieco złagodzić brak entropii, ponieważ sprawia to, że atak brutalny jest znacznie droższy.
Po prostu wiedz, że wybranie krótszego, ale wciąż rozsądnie bezpiecznego hasła nie zepsuje tego schematu, po prostu zmniejszy liczbę możliwych wartości, które atakujący musiałby przeszukać. upewnij się, że wybrałeś wystarczająco silne hasło dla swoich wymagań bezpieczeństwa .
Alternatywy
Niewyraźne
Alternatywą nie jest szyfrowanie . Nie ulegaj pokusie, aby po prostu użyć szyfru o niskim poziomie bezpieczeństwa lub własnej implementacji, powiedzmy Vignere. W tych podejściach nie ma zabezpieczeń, ale może dać niedoświadczonemu programistowi, któremu powierzono zadanie utrzymania twojego kodu w przyszłości, iluzję bezpieczeństwa, która jest gorsza niż brak bezpieczeństwa.
Jeśli potrzebujesz tylko niejasności, po prostu base64 dane; w przypadku wymagań dotyczących bezpieczeństwa adresów URL base64.urlsafe_b64encode()
funkcja jest w porządku. Nie używaj tutaj hasła, po prostu zakoduj i gotowe. Co najwyżej dodaj trochę kompresji (na przykład 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))
To zmienia się b'Hello world!'
w b'eNrzSM3JyVcozy_KSVEEAB0JBF4='
.
Tylko uczciwość
Jeśli potrzebujesz tylko sposobu, aby upewnić się, że dane mogą być niezmienione po wysłaniu do niezaufanego klienta i odebraniu z powrotem, to chcesz podpisać dane, możesz użyć do tego hmac
biblioteki za pomocą SHA1 (nadal uważany za bezpieczny do podpisywania HMAC ) lub lepszy:
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)
Użyj tego do podpisania danych, a następnie dołącz podpis do danych i wyślij go do klienta. Po otrzymaniu danych podziel je i podpis, a następnie zweryfikuj. Ustawiłem domyślny algorytm na SHA256, więc będziesz potrzebować 32-bajtowego klucza:
key = secrets.token_bytes(32)
Możesz zajrzeć do itsdangerous
biblioteki , która pakuje to wszystko z serializacją i deserializacją w różnych formatach.
Korzystanie z szyfrowania AES-GCM w celu zapewnienia szyfrowania i integralności
Fernet opiera się na AEC-CBC z podpisem HMAC, aby zapewnić integralność zaszyfrowanych danych; złośliwy napastnik nie może przesyłać do twojego systemu nonsensownych danych, aby Twoja usługa była zajęta działaniem w kółko ze złymi danymi wejściowymi, ponieważ szyfrogram jest podpisany.
Galois / Licznik szyfr blokowy tryb wytwarza szyfrogram i tag służyć temu samemu celowi, więc może być używany, aby służyć tym samym celom. Wadą jest to, że w przeciwieństwie do Ferneta nie ma łatwego w użyciu uniwersalnego przepisu do ponownego wykorzystania na innych platformach. AES-GCM również nie używa dopełnienia, więc ten zaszyfrowany tekst jest zgodny z długością wiadomości wejściowej (podczas gdy Fernet / AES-CBC szyfruje wiadomości do bloków o stałej długości, nieco zaciemniając długość wiadomości).
AES256-GCM przyjmuje zwykły 32-bajtowy sekret jako klucz:
key = secrets.token_bytes(32)
następnie użyj
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()
Dodałem sygnaturę czasową, aby obsługiwać te same przypadki użycia, które obsługuje Fernet.
Inne podejścia na tej stronie, w Pythonie 3
AES CFB - jak CBC, ale bez potrzeby stosowania podkładek
Jest to podejście, które stosuje All Іѕ Vаиітy , aczkolwiek niepoprawne. To jest cryptography
wersja, ale zauważ, że dołączam IV do zaszyfrowanego tekstu , nie powinien być przechowywany jako globalny (ponowne użycie IV osłabia bezpieczeństwo klucza, a przechowywanie go jako modułu globalnego oznacza, że zostanie on ponownie wygenerowany następne wywołanie Pythona, uniemożliwiające odszyfrowanie całego tekstu zaszyfrowanego):
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()
Brakuje dodatkowego opancerzenia podpisu HMAC i nie ma znacznika czasu; musiałbyś je dodać samodzielnie.
Powyższe pokazuje również, jak łatwo jest niepoprawnie łączyć podstawowe elementy kryptograficzne; Wszystkie niepoprawne obchodzenie się z wartością IV przez Vаиітy może prowadzić do naruszenia danych lub nieczytelności wszystkich zaszyfrowanych wiadomości z powodu utraty IV. Korzystanie z Ferneta chroni Cię przed takimi błędami.
AES EBC - niezabezpieczone
Jeśli wcześniej zaimplementowałeś szyfrowanie AES ECB i nadal potrzebujesz go obsługiwać w Pythonie 3, możesz to zrobić cryptography
również z . Obowiązują te same zastrzeżenia, EBC nie jest wystarczająco bezpieczny dla rzeczywistych zastosowań . Ponowna implementacja tej odpowiedzi dla Pythona 3, dodając automatyczną obsługę wypełnienia:
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()
Ponownie, brakuje podpisu HMAC i i tak nie powinieneś używać EBC. Powyższe ma na celu jedynie zilustrowanie, że cryptography
poradzi sobie z typowymi elementami kryptograficznymi, nawet tymi, których w rzeczywistości nie powinieneś używać.