Szyfruj i odszyfruj za pomocą PyCrypto AES 256

171

Próbuję zbudować dwie funkcje za pomocą PyCrypto, które akceptują dwa parametry: wiadomość i klucz, a następnie zaszyfrują / odszyfrują wiadomość.

Znalazłem w sieci kilka linków, które mogą mi pomóc, ale każdy z nich ma wady:

Ten w codekoala używa os.urandom, którego PyCrypto odradza.

Co więcej, klucz, który podam do funkcji, nie gwarantuje, że będzie miał dokładną oczekiwaną długość. Co mogę zrobić, aby to się stało?

Jest też kilka trybów, który z nich jest zalecany? Nie wiem czego użyć: /

Wreszcie, czym właściwie jest IV? Czy mogę podać inny IV do szyfrowania i odszyfrowywania, czy może to zwróci inny wynik?

Edycja : Usunięto część kodu, ponieważ nie była bezpieczna.

Cyryl N.
źródło
12
os.urandomzachęcani na PyCrypto internetowej. Wykorzystuje funkcję Microsoft CryptGenRandom, która jest CSPRNG
Joel Vroom
5
lub /dev/urandomna Uniksie
Joel Vroom
2
Dla wyjaśnienia, w tym przykładzie hasło jest kluczem, który może mieć 128, 192 lub 256 bitów (16, 24 lub 32 bajty)
Mark
4
Warto wspomnieć, że PyCrypto to martwy projekt . Ostatnie zatwierdzenie pochodzi z 2014 roku. PyCryptodome wygląda na dobry zamiennik typu drop-in
Overdrivr
1
To pytanie jest stare, ale chciałbym zwrócić uwagę (od 2020 r.), Że pycrypto jest prawdopodobnie nieaktualne i nie jest już obsługiwane. Patrząc na ich stronę github ( github.com/pycrypto/pycrypto ), wydaje się, że ich ostatnie zatwierdzenie miało miejsce w 2014 roku. Byłbym nieufny wobec korzystania z oprogramowania kryptograficznego, które nie jest już w fazie rozwoju
irritable_phd_syndrom

Odpowiedzi:

151

Oto moja implementacja i działa dla mnie z kilkoma poprawkami i ulepsza wyrównanie klucza i tajnej frazy z 32 bajtami i iv do 16 bajtów:

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:])]
mnothic
źródło
14
Wiem, że to trwało od jakiegoś czasu, ale myślę, że ta odpowiedź może spowodować pewne zamieszanie. Ta funkcja używa rozmiaru bloku 32 bajtów (256 bajtów) do wypełnienia danych wejściowych, ale AES używa rozmiaru bloku 128-bitowego. W AES256 klucz jest 256-bitowy, ale nie ma rozmiaru bloku.
Tanina
13
innymi słowy, należy usunąć „self.bs” i zastąpić je „AES.block_size”
Alexis
2
Dlaczego haszujesz klucz? Jeśli spodziewasz się, że jest to coś w rodzaju hasła, nie powinieneś używać SHA256; lepiej użyć funkcji wyprowadzania klucza, takiej jak PBKDF2, którą zapewnia PyCrypto.
tweaksp
5
@Chris - SHA256 podaje 32-bajtowy skrót - klucz o idealnej wielkości dla AES256. Zakłada się, że generowanie / wyprowadzanie klucza jest losowe / bezpieczne i powinno znajdować się poza zakresem kodu szyfrowania / deszyfrowania - haszowanie jest tylko gwarancją, że klucz jest użyteczny z wybranym szyfrem.
zwer
2
w _pad self.bs wymagany jest dostęp, aw _unpad nie jest potrzebny
mnothic
149

Możesz potrzebować następujących dwóch funkcji: pad- do unpadblokowania (podczas szyfrowania) i - do usuwania (podczas deszyfrowania), gdy długość danych wejściowych nie jest wielokrotnością 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:])]

Więc pytasz o długość klucza? Możesz użyć sumy md5 klucza, zamiast używać go bezpośrednio.

Co więcej, zgodnie z moim małym doświadczeniem w używaniu PyCrypto, IV jest używany do mieszania danych wyjściowych szyfrowania, gdy dane wejściowe są takie same, więc IV jest wybierany jako losowy ciąg i używa go jako części wyjścia szyfrowania, a następnie użyj go do odszyfrowania wiadomości.

A oto moja implementacja, mam nadzieję, że będzie dla Ciebie przydatna:

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:] ))
Marcus
źródło
1
Co się stanie, jeśli dane wejściowe są dokładnie wielokrotnością BLOCK_SIZE? Myślę, że funkcja unpada byłaby trochę zdezorientowana ...
Kjir
2
@Kjir, do danych źródłowych zostanie dołączona sekwencja wartości chr (BS) o długości BLOCK_SIZE.
Marcus
1
@Marcus padfunkcja jest zepsuta (przynajmniej w Py3), zamień s[:-ord(s[len(s)-1:])]na, aby działała w różnych wersjach.
Torxed
2
Funkcja @Torxed pad jest dostępna w CryptoUtil.Padding.pad () z pycryptodome (pycrypto followup)
comte
2
Dlaczego po prostu nie mieć stałej znaku jako znaku dopełniającego?
Inaimathi
16

Pozwól, że odpowiem na Twoje pytanie dotyczące „trybów”. AES256 to rodzaj szyfru blokowego . Przyjmuje jako dane wejściowe 32-bajtowy klucz i 16-bajtowy ciąg, zwany blokiem i wysyła blok. Używamy AES w trybie pracy w celu szyfrowania. Powyższe rozwiązania sugerują użycie CBC, co jest jednym z przykładów. Inny nazywa się CTR i jest nieco łatwiejszy w użyciu:

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)

Jest to często określane jako AES-CTR. Radziłbym ostrożność w używaniu AES-CBC z PyCrypto . Powodem jest to, że wymaga określenia schematu wypełnienia , czego przykładem są inne podane rozwiązania. Ogólnie rzecz biorąc, jeśli nie jesteś bardzo ostrożny z dopełnieniem, istnieją ataki, które całkowicie łamią szyfrowanie!

Teraz ważne jest, aby pamiętać, że klucz musi być losowym, 32-bajtowym ciągiem ; hasło nie wystarczy. Zwykle klucz jest generowany w następujący sposób:

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

Klucz może również pochodzić z hasła :

# 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)

Niektóre powyższe rozwiązania sugerują użycie SHA256 do wyprowadzenia klucza, ale jest to ogólnie uważane za złą praktykę kryptograficzną . Sprawdź Wikipedię, aby dowiedzieć się więcej o trybach działania.

tweaksp
źródło
iv_int = int (binascii.hexlify (iv), 16) nie działa, zamień go na iv_int = int (binascii.hexlify (iv), 16) plus 'import binascii' i powinno działać (w Pythonie 3.x ), poza tym świetna robota!
Valmond
Należy pamiętać, że lepiej jest używać trybów szyfrowania automatycznego jako AES-GCM. GCM wewnętrznie korzysta z trybu CTR.
kelalaka
Ten kod powoduje „Błąd TypeError: Typ obiektu <class 'str'> nie może być przekazany do kodu C”
Da Woon Jung
7

Dla kogoś, kto chciałby używać urlsafe_b64encode i urlsafe_b64decode, oto wersja, która działa dla mnie (po spędzeniu trochę czasu z problemem 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:]))
Hoang HUA
źródło
6

Możesz uzyskać hasło z dowolnego hasła, używając kryptograficznej funkcji skrótu ( NIE wbudowanej w Pythonie hash), takiej jak SHA-1 lub SHA-256. Python zawiera obsługę obu w swojej standardowej bibliotece:

import hashlib

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

Możesz skrócić kryptograficzną wartość skrótu, używając tylko [:16]lub, [:24]a zachowa ona swoje zabezpieczenia do określonej długości.

nneonneo
źródło
13
Nie powinieneś używać funkcji skrótu z rodziny SHA do generowania klucza z hasła - zobacz esej Cody Hale na ten temat . Zamiast tego rozważ użycie prawdziwej funkcji wyprowadzania klucza, takiej jak scrypt . (Esej Cody Hale'a został napisany przed publikacją
Scrypt
7
Dla przyszłych czytelników, jeśli chcesz uzyskać klucz z hasła, poszukaj PBKDF2. Jest dość łatwy w użyciu w Pythonie ( pypi.python.org/pypi/pbkdf2 ). Jeśli jednak chcesz haszować hasła, lepszą opcją jest bcrypt.
C Fairweather
6

Wdzięczny za inne odpowiedzi, które zainspirowały mnie, ale nie zadziałały.

Po spędzać czas próbuje dowiedzieć się, jak to działa, wpadłem na realizację poniżej z najnowszej PyCryptodomex biblioteki (jest to inna historia, jak udało mi się ustawić go za pełnomocnika, w systemie Windows, w sposób virtualenv .. uff)

pracujących na swojej implementacji pamiętaj, aby zapisać dopełnienie, kodowanie, kroki szyfrowania (i odwrotnie). Trzeba pakować i rozpakowywać pamiętając o kolejności.

import base64
import hashlib
z importu Cryptodome.Cipher AES
z Cryptodome.Random import get_random_bytes

__key__ = hashlib.sha256 (b'16-znakowy klucz '). digest ()

def encrypt (raw):
    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 (klucz = __key__, tryb = AES.MODE_CFB, iv = iv)
    return base64.b64encode (iv + cipher.encrypt (raw))

def odszyfrować (szyfrować):
    unpad = lambda s: s [: - ord (s [-1:])]

    enc = base64.b64decode (enc)
    iv = enc [: AES.block_size]
    cipher = AES.new (__ klucz__, AES.MODE_CFB, iv)
    return unpad (base64.b64decode (cipher.decrypt (enc [AES.block_size:])). decode ('utf8'))
cenkarioz
źródło
Dziękuję za działający przykład z bibliotekami PyCryptodomeX. To bardzo pomocne!
Ygramul
5

Dla dobra innych, oto moja implementacja deszyfrowania, do której doszedłem, łącząc odpowiedzi @Cyril i @Marcus. Zakłada się, że to przychodzi za pośrednictwem żądania HTTP z cytowanym tekstem zaszyfrowanym i zakodowanym w standardzie Base64.

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()
scottmrogowski
źródło
5

Inne podejście do tego (silnie wywodzące się z powyższych rozwiązań), ale

  • używa null do wypełnienia
  • nie używa lambdy (nigdy nie byłem fanem)
  • testowane z Pythonem 2.7 i 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')
MIkee
źródło
To nie zadziała, jeśli bajt wejściowy [] ma końcowe wartości null, ponieważ w funkcji decrypt () zjesz wypełnione wartości NULL ORAZ wszystkie końcowe wartości null.
Buzz Moschetti
Tak, jak stwierdziłem powyżej, ta logika jest wypełniona zerami. Jeśli elementy, które chcesz zakodować / zdekodować, mogą mieć końcowe wartości null, lepiej użyj jednego z innych rozwiązań tutaj
MIkee
3

Użyłem zarówno Cryptoi PyCryptodomexbiblioteki, jak i błyskawicznie ...

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)
Smack Alpha
źródło
2

Jest trochę późno, ale myślę, że będzie to bardzo pomocne. Nikt nie wspomniał o schemacie użycia takim jak dopełnienie PKCS # 7. Możesz go użyć zamiast poprzednich funkcji, aby padać (kiedy robisz szyfrowanie) i unpad (kiedy robisz deszyfrowanie) .i dostarczy pełny kod źródłowy poniżej.

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())

Panagiotis Drakatos
źródło
Nie wiem, kto zlekceważył odpowiedź, ale byłbym ciekawy, dlaczego. Może ta metoda nie jest bezpieczna? Wyjaśnienie byłoby świetne.
Cyril N.
1
@CyrilN. Ta odpowiedź sugeruje, że wystarczy haszowanie hasła jednym wywołaniem SHA-256. Tak nie jest. Naprawdę powinieneś użyć PBKDF2 lub podobnego do wyprowadzenia klucza z hasła przy użyciu dużej liczby iteracji.
Artjom B.
Dziękuję za szczegóły @ArtjomB.!
Cyril N.
Mam klucz, a także klucz IV o długości 44. Jak mogę korzystać z twoich funkcji ?! wszystkie znalezione
przeze
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:])
yuen
źródło
10
Podaj nie tylko kod, ale także wyjaśnij, co robisz i dlaczego jest to lepsze / jaka jest różnica w stosunku do istniejących odpowiedzi.
Florian Koch
Zastąp md5.new (key) .digest () przez md5 (key) .digest () i działa jak urok!
A STEFANI