Python không có lược đồ mã hóa tích hợp, không. Bạn cũng nên coi trọng việc lưu trữ dữ liệu được mã hóa; các lược đồ mã hóa tầm thường mà một nhà phát triển hiểu là không an toàn và một lược đồ đồ chơi cũng có thể bị một nhà phát triển ít kinh nghiệm nhầm với một chương trình an toàn. Nếu bạn mã hóa, hãy mã hóa đúng cách.
Tuy nhiên, bạn không cần phải làm nhiều việc để triển khai một chương trình mã hóa thích hợp. Trước hết, đừng phát minh lại bánh xe mật mã , hãy sử dụng thư viện mật mã đáng tin cậy để xử lý việc này cho bạn. Đối với Python 3, thư viện đáng tin cậy đó là cryptography
.
Tôi cũng khuyến nghị rằng mã hóa và giải mã áp dụng cho các byte ; mã hóa tin nhắn văn bản thành byte trước; stringvalue.encode()
mã hóa thành UTF8, dễ dàng hoàn nguyên lại bằng cách sử dụng bytesvalue.decode()
.
Cuối cùng nhưng không kém phần quan trọng, khi mã hóa và giải mã, chúng ta nói về khóa chứ không phải mật khẩu. Một chìa khóa không được con người ghi nhớ, nó là thứ mà bạn lưu trữ ở một vị trí bí mật nhưng máy có thể đọc được, trong khi mật khẩu thường có thể được con người đọc và ghi nhớ. Bạn có thể lấy một khóa từ mật khẩu, với một chút cẩn thận.
Nhưng đối với một ứng dụng web hoặc tiến trình chạy trong một cụm mà không có sự chú ý của con người để tiếp tục chạy nó, bạn muốn sử dụng một khóa. Mật khẩu dành cho khi chỉ người dùng cuối cần truy cập vào thông tin cụ thể. Ngay cả khi đó, bạn thường bảo mật ứng dụng bằng mật khẩu, sau đó trao đổi thông tin được mã hóa bằng khóa, có lẽ là khóa được gắn với tài khoản người dùng.
Mã hóa khóa đối xứng
Fernet - AES CBC + HMAC, rất khuyến khích
Các cryptography
thư viện bao gồm các công thức Fernet , một công thức thực hành tốt nhất cho việc sử dụng mật mã. Fernet là một tiêu chuẩn mở , với việc triển khai sẵn sàng trong nhiều ngôn ngữ lập trình và nó đóng gói mã hóa AES CBC cho bạn với thông tin phiên bản, dấu thời gian và chữ ký HMAC để ngăn chặn việc giả mạo tin nhắn.
Fernet làm cho nó rất dễ dàng để mã hóa và giải mã tin nhắn và giữ cho bạn an toàn. Đây là phương pháp lý tưởng để mã hóa dữ liệu một cách bí mật.
Tôi khuyên bạn nên sử dụng Fernet.generate_key()
để tạo khóa an toàn. Bạn cũng có thể sử dụng mật khẩu (phần tiếp theo), nhưng khóa bí mật 32 byte đầy đủ (16 byte để mã hóa, cộng với 16 byte khác cho chữ ký) sẽ an toàn hơn hầu hết các mật khẩu bạn có thể nghĩ đến.
Khóa mà Fernet tạo là một bytes
đối tượng có URL và các ký tự base64 an toàn cho tệp, vì vậy có thể in được:
from cryptography.fernet import Fernet
key = Fernet.generate_key() # store in a secure location
print("Key:", key.decode())
Để mã hóa hoặc giải mã thông điệp, hãy tạo một Fernet()
phiên bản với khóa đã cho và gọi Fernet.encrypt()
hoặc Fernet.decrypt()
, cả thông điệp văn bản rõ để mã hóa và mã thông báo được mã hóa đều là bytes
đối tượng.
encrypt()
và các decrypt()
chức năng sẽ giống như sau:
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)
Bản giới thiệu:
>>> 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 với mật khẩu - khóa bắt nguồn từ mật khẩu, làm yếu đi phần nào tính bảo mật
Bạn có thể sử dụng mật khẩu thay vì khóa bí mật, miễn là bạn sử dụng phương pháp dẫn xuất khóa mạnh . Sau đó, bạn phải bao gồm muối và số lần lặp HMAC trong tin nhắn, vì vậy giá trị được mã hóa không còn tương thích với Fernet nữa nếu không tách muối, số và mã thông báo Fernet trước:
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)
Bản giới thiệu:
>>> message = 'John Doe'
>>> password = 'mypass'
>>> password_encrypt(message.encode(), password)
b'9Ljs-w8IRM3XT1NDBbSBuQABhqCAAAAAAFyJdhiCPXms2vQHO7o81xZJn5r8_PAtro8Qpw48kdKrq4vt-551BCUbcErb_GyYRz8SVsu8hxTXvvKOn9QdewRGDfwx'
>>> token = _
>>> password_decrypt(token, password).decode()
'John Doe'
Việc bao gồm muối trong đầu ra làm cho nó có thể sử dụng một giá trị muối ngẫu nhiên, do đó đảm bảo đầu ra được mã hóa được đảm bảo hoàn toàn ngẫu nhiên bất kể việc sử dụng lại mật khẩu hoặc lặp lại tin nhắn. Bao gồm số lần lặp lại đảm bảo rằng bạn có thể điều chỉnh để tăng hiệu suất CPU theo thời gian mà không làm mất khả năng giải mã các thông báo cũ hơn.
Một mật khẩu có thể an toàn như một khóa ngẫu nhiên 32 byte Fernet, miễn là bạn tạo một mật khẩu ngẫu nhiên thích hợp từ một nhóm có kích thước tương tự. 32 byte cung cấp cho bạn 256 ^ 32 số khóa, vì vậy nếu bạn sử dụng bảng chữ cái gồm 74 ký tự (26 chữ hoa, 26 chữ thường, 10 chữ số và 12 ký hiệu có thể có), thì mật khẩu của bạn phải dài ít nhất math.ceil(math.log(256 ** 32, 74))
== 42 ký tự. Tuy nhiên, số lượng lặp lại HMAC lớn hơn được lựa chọn kỹ càng có thể giảm thiểu phần nào việc thiếu entropy vì điều này làm cho kẻ tấn công trở nên tốn kém hơn rất nhiều khi xâm nhập vào.
Chỉ cần biết rằng việc chọn một mật khẩu ngắn hơn nhưng vẫn an toàn hợp lý sẽ không làm tê liệt kế hoạch này, nó chỉ làm giảm số lượng giá trị có thể mà kẻ tấn công bạo lực sẽ phải tìm kiếm; đảm bảo chọn một mật khẩu đủ mạnh cho các yêu cầu bảo mật của bạn .
Giải pháp thay thế
Che khuất
Một giải pháp thay thế là không mã hóa . Vignere nói: Đừng bị cám dỗ chỉ sử dụng mật mã bảo mật thấp hoặc triển khai tại nhà. Không có bảo mật trong các cách tiếp cận này, nhưng có thể khiến một nhà phát triển thiếu kinh nghiệm được giao nhiệm vụ duy trì mã của bạn trong tương lai ảo tưởng về bảo mật, tệ hơn là không có bảo mật nào cả.
Nếu tất cả những gì bạn cần là không rõ ràng, chỉ cần dữ liệu base64; đối với các yêu cầu an toàn cho URL, base64.urlsafe_b64encode()
chức năng vẫn ổn. Không sử dụng mật khẩu ở đây, chỉ cần mã hóa và bạn đã hoàn tất. Tối đa, hãy thêm một số nén (như 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))
Điều này biến b'Hello world!'
thành b'eNrzSM3JyVcozy_KSVEEAB0JBF4='
.
Chỉ toàn vẹn
Nếu tất cả những gì bạn cần là một cách để đảm bảo rằng dữ liệu có thể được tin cậy để không bị thay đổi sau khi được gửi đến một ứng dụng khách không đáng tin cậy và được nhận lại, thì bạn muốn ký dữ liệu, bạn có thể sử dụng hmac
thư viện cho việc này với SHA1 (vẫn được coi là an toàn cho việc ký HMAC ) hoặc tốt hơn:
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)
Sử dụng quyền này để ký dữ liệu, sau đó đính kèm chữ ký với dữ liệu và gửi chữ ký đó cho khách hàng. Khi bạn nhận lại dữ liệu, hãy tách dữ liệu và chữ ký và xác minh. Tôi đã đặt thuật toán mặc định thành SHA256, vì vậy bạn sẽ cần một khóa 32 byte:
key = secrets.token_bytes(32)
Bạn có thể muốn xem itsdangerous
thư viện , gói tất cả điều này với tuần tự hóa và hủy tuần tự hóa ở các định dạng khác nhau.
Sử dụng mã hóa AES-GCM để cung cấp mã hóa và tính toàn vẹn
Fernet xây dựng dựa trên AEC-CBC với chữ ký HMAC để đảm bảo tính toàn vẹn của dữ liệu được mã hóa; kẻ tấn công độc hại không thể cung cấp dữ liệu vô nghĩa cho hệ thống của bạn để giữ cho dịch vụ của bạn luôn hoạt động trong các vòng kết nối với đầu vào không tốt, vì bản mã đã được ký.
Các chế độ Galois / Counter mã khối sản xuất bản mã và thẻ để phục vụ cùng một mục đích, như vậy có thể được sử dụng để phục vụ cho mục đích tương tự. Nhược điểm là không giống như Fernet, không có công thức một kích cỡ phù hợp với tất cả dễ sử dụng để sử dụng lại trên các nền tảng khác. AES-GCM cũng không sử dụng đệm, do đó, bản mã mã hóa này khớp với độ dài của thông điệp đầu vào (trong khi Fernet / AES-CBC mã hóa thông báo thành các khối có độ dài cố định, che khuất độ dài thông báo phần nào).
AES256-GCM lấy bí mật 32 byte thông thường làm khóa:
key = secrets.token_bytes(32)
sau đó sử dụng
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()
Tôi đã bao gồm một dấu thời gian để hỗ trợ các trường hợp sử dụng theo thời gian tồn tại tương tự như Fernet hỗ trợ.
Các cách tiếp cận khác trên trang này, bằng Python 3
AES CFB - giống như CBC nhưng không cần đệm
Đây là cách tiếp cận mà All Іѕ Vаиітy tuân theo, mặc dù không chính xác. Đây là cryptography
phiên bản, nhưng lưu ý rằng tôi đưa IV vào bản mã , nó không nên được lưu trữ dưới dạng toàn cục (việc sử dụng lại IV sẽ làm suy yếu tính bảo mật của khóa và lưu trữ dưới dạng toàn cục mô-đun có nghĩa là nó sẽ được tạo lại lệnh gọi Python tiếp theo, hiển thị tất cả bản mã không thể giải mã được):
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()
Điều này thiếu trang bị bổ sung của chữ ký HMAC và không có dấu thời gian; bạn phải tự thêm chúng.
Phần trên cũng minh họa việc dễ dàng kết hợp các khối xây dựng mật mã cơ bản không chính xác như thế nào; Tất cả việc xử lý sai giá trị IV của Іѕ Vаиітy có thể dẫn đến vi phạm dữ liệu hoặc tất cả các thư được mã hóa không thể đọc được vì IV bị mất. Thay vào đó, sử dụng Fernet sẽ bảo vệ bạn khỏi những sai lầm như vậy.
AES ECB - không an toàn
Nếu trước đây bạn đã triển khai mã hóa AES ECB và vẫn cần hỗ trợ điều này trong Python 3, thì bạn cũng có thể làm như vậy cryptography
. Cảnh báo tương tự cũng được áp dụng, ECB không đủ an toàn cho các ứng dụng trong đời thực . Triển khai lại câu trả lời đó cho Python 3, thêm tính năng tự động xử lý phần đệm:
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()
Một lần nữa, điều này thiếu chữ ký HMAC và bạn không nên sử dụng ECB. Trên đây chỉ là để minh họa rằng cryptography
có thể xử lý các khối xây dựng mật mã phổ biến, ngay cả những khối mà bạn thực sự không nên sử dụng.