Enkripsi & Dekripsi menggunakan PyCrypto AES 256


171

Saya mencoba membangun dua fungsi menggunakan PyCrypto yang menerima dua parameter: pesan dan kunci, dan kemudian mengenkripsi / mendekripsi pesan.

Saya menemukan beberapa tautan di web untuk membantu saya, tetapi masing-masing memiliki kekurangan:

Yang ini di codekoala menggunakan os.urandom, yang tidak disarankan oleh PyCrypto.

Selain itu, kunci yang saya berikan ke fungsi tidak dijamin memiliki panjang yang tepat yang diharapkan. Apa yang bisa saya lakukan untuk mewujudkannya?

Juga, ada beberapa mode, yang mana yang direkomendasikan? Saya tidak tahu harus menggunakan apa: /

Akhirnya, apa sebenarnya IV itu? Dapatkah saya memberikan IV berbeda untuk mengenkripsi dan mendekripsi, atau akankah ini menghasilkan hasil yang berbeda?

Sunting : Menghapus bagian kode karena tidak aman.


12
os.urandom adalah didorong pada PyCrypto situs. Ini menggunakan fungsi CryptGenRandom Microsoft yang merupakan CSPRNG
Joel Vroom

5
atau /dev/urandomdi Unix
Joel Vroom

2
Hanya untuk memperjelas, dalam contoh ini frasa sandi adalah kunci yang bisa 128, 192, atau 256 bit (16, 24, atau 32 byte)
Tandai

4
Mungkin layak untuk disebutkan bahwa PyCrypto adalah proyek mati . Komit terakhir adalah mulai 2014. PyCryptodome terlihat seperti pengganti drop-in yang bagus
Overdrivr

1
Pertanyaan ini sudah lama, tetapi saya ingin menunjukkan (pada tahun 2020) bahwa pycrypto kemungkinan sudah usang dan tidak lagi didukung. Melihat halaman github mereka ( github.com/pycrypto/pycrypto ), tampaknya komit terakhir mereka adalah pada tahun 2014. Saya akan ragu menggunakan perangkat lunak kriptografi yang tidak lagi dalam pengembangan
irritable_phd_syndrom

Jawaban:


151

Berikut ini adalah implementasi dan berfungsi untuk saya dengan beberapa perbaikan dan meningkatkan keselarasan frasa kunci dan rahasia dengan 32 byte dan iv hingga 16 byte:

import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES

class AESCipher(object):

    def __init__(self, key): 
        self.bs = AES.block_size
        self.key = hashlib.sha256(key.encode()).digest()

    def encrypt(self, raw):
        raw = self._pad(raw)
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return base64.b64encode(iv + cipher.encrypt(raw.encode()))

    def decrypt(self, enc):
        enc = base64.b64decode(enc)
        iv = enc[:AES.block_size]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8')

    def _pad(self, s):
        return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)

    @staticmethod
    def _unpad(s):
        return s[:-ord(s[len(s)-1:])]

14
Saya tahu ini sudah cukup lama, tetapi saya pikir respons ini dapat menyebarkan kebingungan. Fungsi ini menggunakan ukuran block_size 32 byte (256 byte) untuk memasukkan data tetapi AES menggunakan ukuran blok 128 bit. Dalam AES256 kuncinya adalah 256 bit, tetapi bukan ukuran blok.
Tannin

13
dengan kata lain, "self.bs" harus dihapus dan diganti oleh "AES.block_size"
Alexis

2
Mengapa Anda hashing kuncinya? Jika Anda mengharapkan ini seperti kata sandi, maka Anda seharusnya tidak menggunakan SHA256; lebih baik menggunakan fungsi derivasi kunci, seperti PBKDF2, yang disediakan PyCrypto.
tweaksp

5
@ Chris - SHA256 memberikan hash 32-byte - kunci berukuran sempurna untuk AES256. Pembuatan / penurunan kunci diasumsikan acak / aman dan harus berada di luar ruang lingkup kode enkripsi / dekripsi - hashing hanyalah jaminan bahwa kunci tersebut dapat digunakan dengan sandi yang dipilih.
zwer

2
di _pad akses self.bs diperlukan dan di _unpad tidak perlu
mnothic

149

Anda mungkin memerlukan dua fungsi berikut: pad- untuk membalut (saat melakukan enkripsi) dan unpad- untuk membatalkan (ketika melakukan dekripsi) ketika panjang input bukan kelipatan dari BLOCK_SIZE.

BS = 16
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS) 
unpad = lambda s : s[:-ord(s[len(s)-1:])]

Jadi Anda menanyakan panjang kunci? Anda dapat menggunakan md5sum dari kunci daripada menggunakannya secara langsung.

Lebih lanjut, menurut sedikit pengalaman saya menggunakan PyCrypto, IV digunakan untuk mencampur output dari sebuah enkripsi ketika input sama, sehingga IV dipilih sebagai string acak, dan menggunakannya sebagai bagian dari output enkripsi, dan kemudian gunakan untuk mendekripsi pesan.

Dan ini implementasi saya, semoga bermanfaat bagi Anda:

import base64
from Crypto.Cipher import AES
from Crypto import Random

class AESCipher:
    def __init__( self, key ):
        self.key = key

    def encrypt( self, raw ):
        raw = pad(raw)
        iv = Random.new().read( AES.block_size )
        cipher = AES.new( self.key, AES.MODE_CBC, iv )
        return base64.b64encode( iv + cipher.encrypt( raw ) ) 

    def decrypt( self, enc ):
        enc = base64.b64decode(enc)
        iv = enc[:16]
        cipher = AES.new(self.key, AES.MODE_CBC, iv )
        return unpad(cipher.decrypt( enc[16:] ))

1
Apa yang terjadi jika Anda memiliki input yang merupakan kelipatan dari BLOCK_SIZE? Saya pikir fungsi unpad akan sedikit bingung ...
Kjir

2
@ Kir, maka urutan nilai chr (BS) panjangnya BLOCK_SIZE akan ditambahkan ke data asal.
Marcus

1
@Marcus padfungsinya rusak (setidaknya di Py3), ganti dengan s[:-ord(s[len(s)-1:])]agar berfungsi di seluruh versi.
Torxed

2
Fungsi pad @Torxed tersedia di CryptoUtil.Padding.pad () dengan pycryptodome (pycrypto followup)
comte

2
Mengapa tidak hanya memiliki karakter konstan sebagai padding char?
Inaimathi

16

Biarkan saya menjawab pertanyaan Anda tentang "mode." AES256 adalah sejenis cipher blok . Dibutuhkan sebagai input kunci 32-byte dan string 16-byte, yang disebut blok dan output blok. Kami menggunakan AES dalam mode operasi untuk mengenkripsi. Solusi di atas menyarankan menggunakan CBC, yang merupakan salah satu contoh. Lain disebut CTR, dan itu agak lebih mudah digunakan:

from Crypto.Cipher import AES
from Crypto.Util import Counter
from Crypto import Random

# AES supports multiple key sizes: 16 (AES128), 24 (AES192), or 32 (AES256).
key_bytes = 32

# Takes as input a 32-byte key and an arbitrary-length plaintext and returns a
# pair (iv, ciphtertext). "iv" stands for initialization vector.
def encrypt(key, plaintext):
    assert len(key) == key_bytes

    # Choose a random, 16-byte IV.
    iv = Random.new().read(AES.block_size)

    # Convert the IV to a Python integer.
    iv_int = int(binascii.hexlify(iv), 16) 

    # Create a new Counter object with IV = iv_int.
    ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)

    # Create AES-CTR cipher.
    aes = AES.new(key, AES.MODE_CTR, counter=ctr)

    # Encrypt and return IV and ciphertext.
    ciphertext = aes.encrypt(plaintext)
    return (iv, ciphertext)

# Takes as input a 32-byte key, a 16-byte IV, and a ciphertext, and outputs the
# corresponding plaintext.
def decrypt(key, iv, ciphertext):
    assert len(key) == key_bytes

    # Initialize counter for decryption. iv should be the same as the output of
    # encrypt().
    iv_int = int(iv.encode('hex'), 16) 
    ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)

    # Create AES-CTR cipher.
    aes = AES.new(key, AES.MODE_CTR, counter=ctr)

    # Decrypt and return the plaintext.
    plaintext = aes.decrypt(ciphertext)
    return plaintext

(iv, ciphertext) = encrypt(key, 'hella')
print decrypt(key, iv, ciphertext)

Ini sering disebut sebagai AES-CTR. Saya akan menyarankan agar berhati-hati dalam menggunakan AES-CBC dengan PyCrypto . Alasannya adalah itu mengharuskan Anda untuk menentukan skema padding , sebagaimana dicontohkan oleh solusi lain yang diberikan. Secara umum, jika Anda tidak terlalu berhati-hati dengan padding, ada serangan yang benar-benar merusak enkripsi!

Sekarang, penting untuk dicatat bahwa kunci tersebut harus berupa string 32 byte acak ; kata sandi tidak cukup. Biasanya, kunci dibuat seperti ini:

# Nominal way to generate a fresh key. This calls the system's random number
# generator (RNG).
key1 = Random.new().read(key_bytes)

Kunci juga dapat diturunkan dari kata sandi :

# It's also possible to derive a key from a password, but it's important that
# the password have high entropy, meaning difficult to predict.
password = "This is a rather weak password."

# For added # security, we add a "salt", which increases the entropy.
#
# In this example, we use the same RNG to produce the salt that we used to
# produce key1.
salt_bytes = 8 
salt = Random.new().read(salt_bytes)

# Stands for "Password-based key derivation function 2"
key2 = PBKDF2(password, salt, key_bytes)

Beberapa solusi di atas menyarankan menggunakan SHA256 untuk mendapatkan kunci, tetapi ini umumnya dianggap praktik kriptografi yang buruk . Lihat wikipedia untuk informasi lebih lanjut tentang mode operasi.


iv_int = int (binascii.hexlify (iv), 16) tidak berfungsi, ganti dengan iv_int = int (binascii.hexlify (iv), 16) ditambah 'import binascii' dan itu akan berfungsi (pada Python 3.x ), kalau tidak kerja bagus!
Valmond

Perhatikan bahwa lebih baik menggunakan mode Enkripsi Autehnticated sebagai AES-GCM. GCM secara internal menggunakan mode CTR.
kelalaka

Kode ini menyebabkan "TypeError: Jenis objek <class 'str'> tidak dapat diteruskan ke kode C"
Da Woon Jung

7

Untuk seseorang yang ingin menggunakan urlsafe_b64encode dan urlsafe_b64decode, berikut adalah versi yang berfungsi untuk saya (setelah menghabiskan waktu dengan masalah unicode)

BS = 16
key = hashlib.md5(settings.SECRET_KEY).hexdigest()[:BS]
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
unpad = lambda s : s[:-ord(s[len(s)-1:])]

class AESCipher:
    def __init__(self, key):
        self.key = key

    def encrypt(self, raw):
        raw = pad(raw)
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return base64.urlsafe_b64encode(iv + cipher.encrypt(raw)) 

    def decrypt(self, enc):
        enc = base64.urlsafe_b64decode(enc.encode('utf-8'))
        iv = enc[:BS]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return unpad(cipher.decrypt(enc[BS:]))

6

Anda bisa mendapatkan frasa sandi dari kata sandi sewenang-wenang dengan menggunakan fungsi hash kriptografis ( BUKAN builtin Python hash) seperti SHA-1 atau SHA-256. Python menyertakan dukungan untuk keduanya di pustaka standarnya:

import hashlib

hashlib.sha1("this is my awesome password").digest() # => a 20 byte string
hashlib.sha256("another awesome password").digest() # => a 32 byte string

Anda dapat memotong nilai hash kriptografis hanya dengan menggunakan [:16]atau [:24]dan itu akan mempertahankan keamanannya selama Anda tentukan.


13
Anda tidak boleh menggunakan fungsi hash keluarga SHA untuk membuat kunci dari kata sandi - lihat esai Coda Hale tentang topik tersebut . Pertimbangkan untuk menggunakan fungsi derivasi kunci nyata seperti scrypt . (Esai Coda Hale ditulis sebelum publikasi scrypt.)
Benjamin Barenblat

7
Untuk pembaca masa depan, jika Anda ingin mendapatkan kunci dari frasa sandi, cari PBKDF2. Ini cukup mudah digunakan dalam python ( pypi.python.org/pypi/pbkdf2 ). Namun, jika Anda mencari kata sandi hash, bcrypt adalah pilihan yang lebih baik.
C Fairweather

6

Bersyukur atas jawaban-jawaban lain yang mengilhami tetapi tidak berhasil bagi saya.

Setelah menghabiskan berjam-jam mencoba mencari cara kerjanya, saya datang dengan implementasi di bawah ini dengan perpustakaan PyCryptodomex terbaru (ini adalah cerita lain bagaimana saya mengaturnya di belakang proxy, pada Windows, dalam virtualenv .. phew)

Mengerjakan implementasi Anda, ingatlah untuk menulis padding, encoding, mengenkripsi langkah-langkah (dan sebaliknya). Anda harus berkemas dan membongkar dengan mengingat pesanan.

impor basis64
impor hashlib
dari impor Cryptodome.Cipher AES
dari Cryptodome.Random mengimpor get_random_bytes

__key__ = hashlib.sha256 (b'16-character key '). digest ()

def mengenkripsi (mentah):
    BS = AES.block_size
    pad = lambda s: s + (BS - len (s)% BS) * chr (BS - len (s)% BS)

    raw = base64.b64encode (pad (raw) .encode ('utf8'))
    iv = get_random_bytes (AES.block_size)
    cipher = AES.new (key = __key__, mode = AES.MODE_CFB, iv = iv)
    return base64.b64encode (iv + cipher.encrypt (raw))

def decrypt (enc):
    unpad = lambda s: s [: - ord (s [-1:])]

    enc = base64.b64decode (enc)
    iv = enc [: AES.block_size]
    cipher = AES.new (__ key__, AES.MODE_CFB, iv)
    return unpad (base64.b64decode (cipher.decrypt (enc [AES.block_size:])). decode ('utf8'))

Terima kasih atas contoh yang berfungsi dengan libs PyCryptodomeX. Itu cukup membantu!
Ygramul

5

Demi kepentingan orang lain, inilah implementasi dekripsi saya yang saya dapat dengan menggabungkan jawaban dari @Cyril dan @Marcus. Ini mengasumsikan bahwa ini datang melalui Permintaan HTTP dengan kutipan dienkripsi dan base64 disandikan.

import base64
import urllib2
from Crypto.Cipher import AES


def decrypt(quotedEncodedEncrypted):
    key = 'SecretKey'

    encodedEncrypted = urllib2.unquote(quotedEncodedEncrypted)

    cipher = AES.new(key)
    decrypted = cipher.decrypt(base64.b64decode(encodedEncrypted))[:16]

    for i in range(1, len(base64.b64decode(encodedEncrypted))/16):
        cipher = AES.new(key, AES.MODE_CBC, base64.b64decode(encodedEncrypted)[(i-1)*16:i*16])
        decrypted += cipher.decrypt(base64.b64decode(encodedEncrypted)[i*16:])[:16]

    return decrypted.strip()

5

Lain mengambil ini (sangat berasal dari solusi di atas) tetapi

  • menggunakan null untuk bantalan
  • tidak menggunakan lambda (tidak pernah menjadi penggemar)
  • diuji dengan python 2.7 dan 3.6.5

    #!/usr/bin/python2.7
    # you'll have to adjust for your setup, e.g., #!/usr/bin/python3
    
    
    import base64, re
    from Crypto.Cipher import AES
    from Crypto import Random
    from django.conf import settings
    
    class AESCipher:
        """
          Usage:
          aes = AESCipher( settings.SECRET_KEY[:16], 32)
          encryp_msg = aes.encrypt( 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppp' )
          msg = aes.decrypt( encryp_msg )
          print("'{}'".format(msg))
        """
        def __init__(self, key, blk_sz):
            self.key = key
            self.blk_sz = blk_sz
    
        def encrypt( self, raw ):
            if raw is None or len(raw) == 0:
                raise NameError("No value given to encrypt")
            raw = raw + '\0' * (self.blk_sz - len(raw) % self.blk_sz)
            raw = raw.encode('utf-8')
            iv = Random.new().read( AES.block_size )
            cipher = AES.new( self.key.encode('utf-8'), AES.MODE_CBC, iv )
            return base64.b64encode( iv + cipher.encrypt( raw ) ).decode('utf-8')
    
        def decrypt( self, enc ):
            if enc is None or len(enc) == 0:
                raise NameError("No value given to decrypt")
            enc = base64.b64decode(enc)
            iv = enc[:16]
            cipher = AES.new(self.key.encode('utf-8'), AES.MODE_CBC, iv )
            return re.sub(b'\x00*$', b'', cipher.decrypt( enc[16:])).decode('utf-8')

Ini tidak akan berfungsi jika byte input [] memiliki trailing nulls karena dalam fungsi decrypt () Anda akan memakan padding nulls Anda PLUS setiap trailing nulls.
Buzz Moschetti

Ya, seperti yang saya nyatakan di atas, logika ini cocok dengan nol. Jika item yang Anda ingin encode / decode mungkin tertinggal nol, lebih baik gunakan salah satu solusi lain di sini
MIkee

3

Saya telah menggunakan keduanya Cryptodan PyCryptodomexperpustakaan dan sangat cepat ...

import base64
import hashlib
from Cryptodome.Cipher import AES as domeAES
from Cryptodome.Random import get_random_bytes
from Crypto import Random
from Crypto.Cipher import AES as cryptoAES

BLOCK_SIZE = AES.block_size

key = "my_secret_key".encode()
__key__ = hashlib.sha256(key).digest()
print(__key__)

def encrypt(raw):
    BS = cryptoAES.block_size
    pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
    raw = base64.b64encode(pad(raw).encode('utf8'))
    iv = get_random_bytes(cryptoAES.block_size)
    cipher = cryptoAES.new(key= __key__, mode= cryptoAES.MODE_CFB,iv= iv)
    a= base64.b64encode(iv + cipher.encrypt(raw))
    IV = Random.new().read(BLOCK_SIZE)
    aes = domeAES.new(__key__, domeAES.MODE_CFB, IV)
    b = base64.b64encode(IV + aes.encrypt(a))
    return b

def decrypt(enc):
    passphrase = __key__
    encrypted = base64.b64decode(enc)
    IV = encrypted[:BLOCK_SIZE]
    aes = domeAES.new(passphrase, domeAES.MODE_CFB, IV)
    enc = aes.decrypt(encrypted[BLOCK_SIZE:])
    unpad = lambda s: s[:-ord(s[-1:])]
    enc = base64.b64decode(enc)
    iv = enc[:cryptoAES.block_size]
    cipher = cryptoAES.new(__key__, cryptoAES.MODE_CFB, iv)
    b=  unpad(base64.b64decode(cipher.decrypt(enc[cryptoAES.block_size:])).decode('utf8'))
    return b

encrypted_data =encrypt("Hi Steven!!!!!")
print(encrypted_data)
print("=======")
decrypted_data = decrypt(encrypted_data)
print(decrypted_data)

2

Ini sedikit terlambat tetapi saya pikir ini akan sangat membantu. Tidak ada yang menyebutkan tentang skema penggunaan seperti padding PKCS # 7. Anda dapat menggunakannya sebagai ganti fungsi sebelumnya untuk pad (saat melakukan enkripsi) dan membatalkan (ketika melakukan dekripsi) .i akan memberikan Kode Sumber lengkap di bawah ini.

import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES
import pkcs7
class Encryption:

    def __init__(self):
        pass

    def Encrypt(self, PlainText, SecurePassword):
        pw_encode = SecurePassword.encode('utf-8')
        text_encode = PlainText.encode('utf-8')

        key = hashlib.sha256(pw_encode).digest()
        iv = Random.new().read(AES.block_size)

        cipher = AES.new(key, AES.MODE_CBC, iv)
        pad_text = pkcs7.encode(text_encode)
        msg = iv + cipher.encrypt(pad_text)

        EncodeMsg = base64.b64encode(msg)
        return EncodeMsg

    def Decrypt(self, Encrypted, SecurePassword):
        decodbase64 = base64.b64decode(Encrypted.decode("utf-8"))
        pw_encode = SecurePassword.decode('utf-8')

        iv = decodbase64[:AES.block_size]
        key = hashlib.sha256(pw_encode).digest()

        cipher = AES.new(key, AES.MODE_CBC, iv)
        msg = cipher.decrypt(decodbase64[AES.block_size:])
        pad_text = pkcs7.decode(msg)

        decryptedString = pad_text.decode('utf-8')
        return decryptedString

import StringIO
import binascii


def decode(text, k=16):
    nl = len(text)
    val = int(binascii.hexlify(text[-1]), 16)
    if val > k:
        raise ValueError('Input is not padded or padding is corrupt')

    l = nl - val
    return text[:l]


def encode(text, k=16):
    l = len(text)
    output = StringIO.StringIO()
    val = k - (l % k)
    for _ in xrange(val):
        output.write('%02x' % val)
    return text + binascii.unhexlify(output.getvalue())


Saya tidak tahu siapa yang menurunkan jawaban tetapi saya ingin tahu mengapa. Mungkin metode ini tidak aman? Penjelasan akan sangat bagus.
Cyril N.

1
@CyrilN. Jawaban ini menunjukkan bahwa hashing kata sandi dengan satu doa SHA-256 sudah cukup. Bukan itu. Anda benar-benar harus menggunakan PBKDF2 atau yang serupa untuk penurunan kunci dari kata sandi menggunakan jumlah iterasi yang besar.
Artjom B.

Terima kasih atas detailnya @ ArtjomB.!
Cyril N.

Saya punya kunci dan juga kunci iv dengan panjang 44. Bagaimana saya bisa menggunakan fungsi Anda ?! semua algoritma di internet yang saya temukan, memiliki masalah dengan panjang kunci vektor saya
mahshid.r


1
from Crypto import Random
from Crypto.Cipher import AES
import base64

BLOCK_SIZE=16
def trans(key):
     return md5.new(key).digest()

def encrypt(message, passphrase):
    passphrase = trans(passphrase)
    IV = Random.new().read(BLOCK_SIZE)
    aes = AES.new(passphrase, AES.MODE_CFB, IV)
    return base64.b64encode(IV + aes.encrypt(message))

def decrypt(encrypted, passphrase):
    passphrase = trans(passphrase)
    encrypted = base64.b64decode(encrypted)
    IV = encrypted[:BLOCK_SIZE]
    aes = AES.new(passphrase, AES.MODE_CFB, IV)
    return aes.decrypt(encrypted[BLOCK_SIZE:])

10
Berikan tidak hanya kode tetapi juga jelaskan apa yang Anda lakukan dan mengapa ini lebih baik / apa bedanya dengan jawaban yang ada.
Florian Koch

Ganti md5.new (kunci) .digest () oleh md5 (kunci) .digest (), dan itu berfungsi seperti pesona!
A STEFANI
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.