Python tidak memiliki skema enkripsi built-in, tidak. Anda juga harus menganggap serius penyimpanan data terenkripsi; Skema enkripsi sepele yang dipahami oleh satu pengembang sebagai tidak aman dan skema mainan mungkin disalahartikan sebagai skema aman oleh pengembang yang kurang berpengalaman. Jika Anda mengenkripsi, enkripsi dengan benar.
Namun, Anda tidak perlu melakukan banyak pekerjaan untuk menerapkan skema enkripsi yang tepat. Pertama-tama, jangan menemukan kembali roda kriptografi , gunakan pustaka kriptografi tepercaya untuk menangani ini untuk Anda. Untuk Python 3, pustaka tepercaya itu adalah cryptography
.
Saya juga merekomendasikan bahwa enkripsi dan dekripsi berlaku untuk byte ; menyandikan pesan teks menjadi byte terlebih dahulu; stringvalue.encode()
menyandikan ke UTF8, dengan mudah dikembalikan lagi menggunakan bytesvalue.decode()
.
Last but not least, ketika mengenkripsi dan mendekripsi, kita berbicara tentang kunci , bukan kata sandi. Kunci tidak boleh mudah diingat manusia, ini adalah sesuatu yang Anda simpan di lokasi rahasia tetapi dapat dibaca oleh mesin, sedangkan kata sandi seringkali dapat dibaca dan diingat oleh manusia. Anda dapat memperoleh kunci dari kata sandi, dengan sedikit hati-hati.
Tetapi untuk aplikasi web atau proses yang berjalan dalam cluster tanpa perhatian manusia untuk tetap menjalankannya, Anda ingin menggunakan kunci. Kata sandi digunakan ketika hanya pengguna akhir yang membutuhkan akses ke informasi tertentu. Meski begitu, Anda biasanya mengamankan aplikasi dengan kata sandi, kemudian bertukar informasi terenkripsi menggunakan kunci, mungkin yang dilampirkan ke akun pengguna.
Enkripsi kunci simetris
Fernet - AES CBC + HMAC, sangat disarankan
The cryptography
perpustakaan meliputi resep Fernet , resep-praktek terbaik untuk menggunakan kriptografi. Fernet adalah standar terbuka , dengan implementasi siap dalam berbagai bahasa pemrograman dan paket enkripsi AES CBC untuk Anda dengan informasi versi, stempel waktu, dan tanda tangan HMAC untuk mencegah gangguan pesan.
Fernet membuatnya sangat mudah untuk mengenkripsi dan mendekripsi pesan dan membuat Anda tetap aman. Ini adalah metode ideal untuk mengenkripsi data dengan rahasia.
Saya sarankan Anda menggunakan Fernet.generate_key()
untuk menghasilkan kunci aman. Anda juga dapat menggunakan kata sandi (bagian selanjutnya), tetapi kunci rahasia 32-byte penuh (16 byte untuk dienkripsi, ditambah 16 byte lainnya untuk tanda tangan) akan lebih aman daripada kebanyakan kata sandi yang dapat Anda pikirkan.
Kunci yang dihasilkan Fernet adalah bytes
objek dengan URL dan karakter base64 yang aman untuk file, sehingga dapat dicetak:
from cryptography.fernet import Fernet
key = Fernet.generate_key() # store in a secure location
print("Key:", key.decode())
Untuk mengenkripsi atau mendekripsi pesan, buat Fernet()
instance dengan kunci yang diberikan, dan panggil Fernet.encrypt()
atau Fernet.decrypt()
, pesan teks biasa untuk dienkripsi dan token yang dienkripsi adalah bytes
objek.
encrypt()
dan decrypt()
fungsinya akan terlihat seperti:
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 dengan kata sandi - kunci yang berasal dari kata sandi, agak melemahkan keamanan
Anda dapat menggunakan sandi sebagai ganti kunci rahasia, asalkan Anda menggunakan metode derivasi kunci yang kuat . Anda kemudian harus menyertakan salt dan hitungan iterasi HMAC dalam pesan, sehingga nilai yang dienkripsi tidak lagi kompatibel dengan Fernet tanpa terlebih dahulu memisahkan token salt, count dan 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)
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'
Menyertakan salt dalam output memungkinkan untuk menggunakan nilai salt acak, yang pada gilirannya memastikan output terenkripsi dijamin sepenuhnya acak terlepas dari penggunaan ulang sandi atau pengulangan pesan. Menyertakan hitungan iterasi memastikan bahwa Anda dapat menyesuaikan agar kinerja CPU meningkat seiring waktu tanpa kehilangan kemampuan untuk mendekripsi pesan yang lebih lama.
Kata sandi saja bisa seaman kunci acak Fernet 32-byte, asalkan Anda membuat kata sandi acak yang benar dari kumpulan ukuran yang sama. 32 byte memberi Anda 256 ^ 32 jumlah kunci, jadi jika Anda menggunakan alfabet dengan 74 karakter (26 atas, 26 lebih rendah, 10 digit dan 12 simbol yang mungkin), maka kata sandi Anda harus setidaknya math.ceil(math.log(256 ** 32, 74))
== 42 karakter. Namun, sejumlah besar iterasi HMAC yang dipilih dengan baik dapat mengurangi kurangnya entropi karena ini membuatnya jauh lebih mahal bagi penyerang untuk memaksa masuk secara brutal.
Ketahuilah bahwa memilih kata sandi yang lebih pendek tetapi masih cukup aman tidak akan melumpuhkan skema ini, ini hanya mengurangi jumlah kemungkinan nilai yang harus dicari oleh penyerang brute force; pastikan untuk memilih kata sandi yang cukup kuat untuk kebutuhan keamanan Anda .
Alternatif
Mengaburkan
Alternatifnya adalah tidak mengenkripsi . Jangan tergoda untuk hanya menggunakan sandi dengan keamanan rendah, atau implementasi rumahan, katakanlah Vignere. Tidak ada keamanan dalam pendekatan ini, tetapi dapat memberikan pengembang yang tidak berpengalaman yang diberi tugas untuk menjaga kode Anda di masa depan ilusi keamanan, yang lebih buruk daripada tidak ada keamanan sama sekali.
Jika yang Anda butuhkan hanyalah ketidakjelasan, cukup base64 datanya; untuk persyaratan aman URL, base64.urlsafe_b64encode()
fungsinya baik-baik saja. Jangan gunakan kata sandi di sini, cukup encode dan Anda selesai. Paling banyak, tambahkan beberapa kompresi (seperti 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))
Ini berubah b'Hello world!'
menjadi b'eNrzSM3JyVcozy_KSVEEAB0JBF4='
.
Integritas saja
Jika yang Anda butuhkan hanyalah cara untuk memastikan bahwa data dapat dipercaya tidak diubah setelah dikirim ke klien yang tidak tepercaya dan diterima kembali, maka Anda ingin menandatangani data, Anda dapat menggunakan hmac
perpustakaan untuk ini dengan SHA1 (masih dianggap aman untuk penandatanganan HMAC ) atau lebih baik:
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)
Gunakan ini untuk menandatangani data, lalu lampirkan tanda tangan dengan data dan kirimkan ke klien. Saat Anda menerima data kembali, pisahkan data dan tanda tangan dan verifikasi. Saya telah menetapkan algoritme default ke SHA256, jadi Anda memerlukan kunci 32-byte:
key = secrets.token_bytes(32)
Anda mungkin ingin melihat itsdangerous
perpustakaan , yang mengemas semua ini dengan serialisasi dan de-serialisasi dalam berbagai format.
Menggunakan enkripsi AES-GCM untuk memberikan enkripsi dan integritas
Fernet mengembangkan AEC-CBC dengan tanda tangan HMAC untuk memastikan integritas data terenkripsi; penyerang jahat tidak dapat memberi makan data sistem Anda yang tidak masuk akal untuk membuat layanan Anda sibuk berjalan berputar-putar dengan masukan yang buruk, karena ciphertext ditandatangani.
The blok modus Galois / Kontra cipher menghasilkan ciphertext dan tag untuk melayani tujuan yang sama, sehingga dapat digunakan untuk melayani tujuan yang sama. Kelemahannya adalah tidak seperti Fernet, tidak ada resep satu ukuran untuk semua yang mudah digunakan untuk digunakan kembali di platform lain. AES-GCM juga tidak menggunakan padding, jadi ciphertext enkripsi ini cocok dengan panjang pesan input (sedangkan Fernet / AES-CBC mengenkripsi pesan ke blok dengan panjang tetap, agak mengaburkan panjang pesan).
AES256-GCM menggunakan rahasia 32 byte biasa sebagai kunci:
key = secrets.token_bytes(32)
lalu gunakan
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()
Saya telah menyertakan stempel waktu untuk mendukung kasus penggunaan waktu-ke-langsung yang didukung Fernet.
Pendekatan lain di halaman ini, dengan Python 3
AES CFB - seperti CBC tetapi tanpa perlu bantalan
Ini adalah pendekatan yang diikuti All Іѕ Vаиітy , meskipun salah. Ini adalah cryptography
versinya, tetapi perhatikan bahwa saya menyertakan IV di ciphertext , itu tidak boleh disimpan sebagai global (menggunakan kembali IV melemahkan keamanan kunci, dan menyimpannya sebagai modul global berarti itu akan dihasilkan kembali permintaan Python berikutnya, membuat semua ciphertext tidak dapat dienkripsi):
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()
Ini tidak memiliki pelindung tambahan dari tanda tangan HMAC dan tidak ada stempel waktu; Anda harus menambahkannya sendiri.
Hal di atas juga menggambarkan betapa mudahnya menggabungkan blok penyusun kriptografi dasar secara tidak benar; Semua penanganan Vаиітy yang salah atas nilai IV dapat menyebabkan pelanggaran data atau semua pesan terenkripsi tidak dapat dibaca karena IV hilang. Menggunakan Fernet malah melindungi Anda dari kesalahan semacam itu.
AES ECB - tidak aman
Jika sebelumnya Anda menerapkan enkripsi AES ECB dan masih perlu mendukungnya di Python 3, Anda juga dapat melakukannya cryptography
. Peringatan yang sama berlaku, ECB tidak cukup aman untuk aplikasi kehidupan nyata . Mengimplementasikan ulang jawaban itu untuk Python 3, menambahkan penanganan padding otomatis:
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()
Sekali lagi, ini tidak memiliki tanda tangan HMAC, dan Anda tidak boleh menggunakan ECB. Hal di atas hanya untuk ilustrasi yang cryptography
dapat menangani blok penyusun kriptografi umum, bahkan yang seharusnya tidak Anda gunakan.