Шифрование и дешифрование с использованием PyCrypto AES 256

171

Я пытаюсь построить две функции с использованием PyCrypto, которые принимают два параметра: сообщение и ключ, а затем шифруют / дешифруют сообщение.

Я нашел несколько ссылок в Интернете, чтобы помочь мне, но у каждой из них есть недостатки:

Этот в codekoala использует os.urandom , что не приветствуется PyCrypto.

Более того, ключ, который я даю этой функции, не обязательно будет иметь ожидаемую точную длину. Что я могу сделать, чтобы это произошло?

Также есть несколько режимов, какой рекомендуется? Я не знаю, что использовать: /

Наконец, что именно IV? Могу ли я предоставить другой IV для шифрования и дешифрования, или это даст другой результат?

Редактировать : Удалена часть кода, так как она не была безопасной.

Кирилл Н.
источник
12
os.urandom будет поощряться на PyCrypto сайте. Он использует функцию Microsoft CryptGenRandom, которая является CSPRNG
Джоэл Врум
5
или /dev/urandomна Unix
Джоэл Врум
2
Просто для пояснения, в этом примере парольная фраза - это ключ, который может быть 128, 192 или 256 бит (16, 24 или 32 байта)
Марк
4
Возможно, стоит упомянуть, что PyCrypto - мертвый проект . Последний коммит с 2014 года. PyCryptodome выглядит как хорошая замена
Overdrivr
1
Этот вопрос старый, но я хотел бы отметить (по состоянию на 2020 год), что pycrypto, вероятно, устарел и больше не поддерживается. Глядя на их страницу github ( github.com/pycrypto/pycrypto ), кажется, что их последний коммит был в 2014 году. Я бы с подозрением использовал криптографическое программное обеспечение, которое больше не находится в стадии разработки
angryable_phd_syndrom

Ответы:

151

Вот моя реализация и работает для меня с некоторыми исправлениями и улучшает выравнивание ключевой и секретной фразы с 32 байтами и iv - 16 байтами:

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
источник
14
Я знаю, что это было некоторое время, но я думаю, что этот ответ может вызвать некоторую путаницу. Эта функция использует block_size 32 байта (256 байт) для заполнения входных данных, но AES использует 128-битный размер блока. В AES256 ключ 256 бит, но не размер блока.
Танин
13
другими словами, «self.bs» должен быть удален и заменен на «AES.block_size»
Алексис
2
Почему ты хешируешь ключ? Если вы ожидаете, что это что-то вроде пароля, то вам не следует использовать SHA256; Лучше использовать функцию получения ключа, например PBKDF2, которую предоставляет PyCrypto.
tweaksp
5
@Chris - SHA256 выдает 32-байтовый хеш - ключ идеального размера для AES256. Генерирование / получение ключа предполагается случайным / безопасным и должно выходить за рамки кода шифрования / дешифрования - хеширование является лишь гарантией того, что ключ может использоваться с выбранным шифром.
Цвер
2
в _pad self.bs нужен доступ, а в _unpad не нужен
mnothic
149

Вам могут понадобиться следующие две функции: pad- unpadзаполнять (при шифровании) и - освобождать (при расшифровке), когда длина ввода не кратна 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:])]

Так вы спрашиваете длину ключа? Вы можете использовать md5sum ключа, а не использовать его напрямую.

Более того, согласно моему небольшому опыту использования PyCrypto, IV используется для того, чтобы смешивать вывод шифрования, когда ввод одинаковый, поэтому IV выбирается в качестве случайной строки и используется как часть вывода шифрования, а затем используйте его для расшифровки сообщения.

А вот и моя реализация, надеюсь, она вам пригодится:

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
Что произойдет, если у вас есть входное значение, кратное BLOCK_SIZE? Я думаю, что функция unpad немного запуталась ...
Kjir
2
@Kjir, тогда последовательность значений chr (BS) длиной BLOCK_SIZE будет добавлена ​​к исходным данным.
Маркус
1
@Marcus padфункция не работает (по крайней мере, в Py3), замените s[:-ord(s[len(s)-1:])]ее, чтобы она работала между версиями.
Torxed
2
@Torxed функция колодки сл в CryptoUtil.Padding.pad () с pycryptodome (PyCrypto катамнестического)
Конте
2
Почему бы просто не иметь символьную константу в качестве заполнителя?
Инамати
16

Позвольте мне ответить на ваш вопрос о «режимах». AES256 является своего рода блочным шифром . В качестве входных данных он принимает 32-байтовый ключ и 16-байтовую строку, называемую блоком, и выводит блок. Мы используем AES в режиме работы для шифрования. Приведенные выше решения предлагают использовать CBC, что является одним из примеров. Другой называется CTR, и его несколько проще использовать:

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)

Это часто называют AES-CTR. Я бы посоветовал осторожность при использовании AES-CBC с PyCrypto . Причина в том, что для этого требуется указать схему заполнения , что подтверждается другими приведенными решениями. В общем, если вы не очень осторожны с заполнением, существуют атаки, которые полностью нарушают шифрование!

Теперь важно отметить, что ключ должен быть случайной 32-байтовой строкой ; пароль не достаточно. Обычно ключ генерируется так:

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

Ключ также может быть получен из пароля :

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

Некоторые решения выше предлагают использовать SHA256 для получения ключа, но это обычно считается плохой криптографической практикой . Проверьте Википедию для получения дополнительной информации о режимах работы.

tweaksp
источник
iv_int = int (binascii.hexlify (iv), 16) не работает, замените его на iv_int = int (binascii.hexlify (iv), 16) плюс «import binascii», и он должен работать (на Python 3.x ) иначе отличная работа!
Valmond
Обратите внимание, что в качестве AES-GCM лучше использовать режимы Autehnticated Encryption. GCM внутренне использует режим CTR.
Келалака
Этот код вызывает «TypeError: Тип объекта <class 'str'> не может быть передан в код C»
Да Вун Юнг
7

Для тех, кто хотел бы использовать urlsafe_b64encode и urlsafe_b64decode, вот версия, которая работает для меня (после того, как я потратил некоторое время на проблему с юникодом)

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

Вы можете получить кодовую фразу из произвольного пароля, используя криптографическую хеш-функцию ( НЕ встроенную в Python hash), такую ​​как SHA-1 или SHA-256. Python включает поддержку обоих в своей стандартной библиотеке:

import hashlib

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

Вы можете усечь криптографическое значение хеша, просто используя [:16]или, [:24]и он сохранит свою безопасность до указанной вами длины.

nneonneo
источник
13
Вы не должны использовать хеш-функцию семейства SHA для генерации ключа из пароля - см . Эссе Coda Hale на эту тему . Подумайте об использовании реальной функции получения ключей, такой как scrypt . (Эссе Коды Хейл было написано до публикации Скрипта.)
Бенджамин Баренблат
7
Для будущих читателей, если вы хотите получить ключ из ключевой фразы, поищите PBKDF2. Это довольно легко использовать в python ( pypi.python.org/pypi/pbkdf2 ). Однако, если вы хотите хешировать пароли, лучше использовать bcrypt.
C Fairweather
6

Благодарен за другие ответы, которые вдохновили, но не сработали для меня.

Потратив часы на то, чтобы выяснить, как это работает, я пришел к описанной ниже реализации с новейшей библиотекой PyCryptodomex (это еще одна история о том, как мне удалось настроить ее на прокси-сервере, в Windows, в virtualenv .. тьфу).

Работа над Ваша реализация, не забудьте записать шаги заполнения, кодирования, шифрования (и наоборот). Вы должны упаковать и распаковать, учитывая порядок.

импорт base64
импорт хешлиб
от Cryptodome. Шифрование импорта AES
из Cryptodome. Случайный импорт get_random_bytes

__key__ = hashlib.sha256 (b'16-символьный ключ '). digest ()

def encrypt (raw):
    BS = AES.block_size
    pad = лямбда 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)
    шифр = AES.new (ключ = __key__, режим = AES.MODE_CFB, iv = iv)
    вернуть base64.b64encode (iv + cipher.encrypt (raw))

def decrypt (enc):
    unpad = лямбда s: s [: - ord (s [-1:])]

    enc = base64.b64decode (enc)
    iv = enc [: AES.block_size]
    cipher = AES.new (__ key__, AES.MODE_CFB, iv)
    вернуть unpad (base64.b64decode (cipher.decrypt (enc [AES.block_size:])). decode ('utf8'))
cenkarioz
источник
Спасибо за пример работы с библиотеками PyCryptodomeX. Это довольно полезно!
Ygramul
5

Для пользы других, вот моя реализация расшифровки, к которой я пришел, объединив ответы @Cyril и @Marcus. Это предполагает, что это приходит через HTTP-запрос с зашифрованным текстом в кавычках и в кодировке 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
источник
5

Еще один взгляд на это (в значительной степени вытекает из решений выше), но

  • использует нуль для заполнения
  • не использует лямбду (никогда не был фанатом)
  • протестировано на python 2.7 и 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
источник
Это не будет работать, если входной байт [] имеет конечные нули, потому что в функции decrypt () вы будете использовать нулевые отступы плюс любые конечные нули.
Buzz Moschetti
Да, как я утверждаю выше, эта логика дополняется нулями. Если элементы, которые вы хотите кодировать / декодировать, могут иметь завершающие нули, лучше используйте одно из других решений здесь
MIkee
3

Я использовал Cryptoи PyCryptodomexбиблиотеку, и она быстро сверкает ...

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

Уже немного поздно, но я думаю, что это будет очень полезно. Никто не упомянул об использовании схемы, такой как заполнение PKCS # 7. Вы можете использовать его вместо предыдущих функций для заполнения (когда делаете шифрование) и unpad (когда делаете расшифровку) .i предоставит полный исходный код ниже.

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

Панагиотис Дракатос
источник
Я не знаю, кто отклонил ответ, но мне было бы интересно узнать почему. Может быть, этот метод небезопасен? Объяснение было бы здорово.
Кирилл Н.
1
@CyrilN. Этот ответ предполагает, что хеширования пароля с помощью одного вызова SHA-256 достаточно. Это не так. Вы действительно должны использовать PBKDF2 или аналогичный для получения ключа из пароля с использованием большого количества итераций.
Artjom B.
Спасибо за детали @ArtjomB.!
Кирилл Н.
У меня есть ключ, а также iv ключ длиной 44. Как я могу использовать ваши функции ?! у всех алгоритмов в интернете, которые я нашел, есть проблема с длиной моего векторного ключа
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
Пожалуйста, предоставьте не только код, но и объясните, что вы делаете и почему это лучше / в чем разница с существующими ответами.
Флориан Кох
Замените md5.new (ключ) .digest () на md5 (ключ) .digest (), и это сработает как шарм!
A STEFANI