Шифруйте та розшифруйте за допомогою PyCrypto AES 256


171

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

Я знайшов кілька посилань в Інтернеті, щоб допомогти мені, але кожен з них має вади:

Цей в кодекоалі використовує os.urandom, який відлякує PyCrypto.

Більше того, ключ, який я даю функції, не гарантує точно очікувану довжину. Що я можу зробити, щоб це сталося?

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

Нарешті, що саме таке IV? Чи можу я надати інший ІV для шифрування та дешифрування, чи це повернеться до іншого результату?

Редагувати : Видалено кодову частину, оскільки вона не була захищеною.


12
os.urandom буде заохочуватися на PyCrypto сайті. Він використовує функцію CryptGenRandom від Microsoft, яка є CSPRNG
Joel Vroom

5
або /dev/urandomна Unix
Joel Vroom

2
Просто для уточнення, у цьому прикладі парольна фраза - це ключ, який може бути 128, 192 або 256 біт (16, 24 або 32 байти)
Марк

4
Можливо, варто згадати, що PyCrypto - це мертвий проект . Остання фіксація - з 2014 року. PyCryptodome виглядає як гарна заміна
Overdrivr

1
Це питання давнє, але я хотів би зазначити (станом на 2020 рік), що пікрипто, ймовірно, застарів і більше не підтримується. Переглядаючи їхню сторінку github ( github.com/pycrypto/pycrypto ), схоже, їх останній вчинок був у 2014 році. Мені слід було б використовувати криптографічне програмне забезпечення, яке вже не розробляється
irritable_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:])]

14
Я знаю, що це тривало деякий час, але я думаю, що ця реакція може поширити деяку плутанину. Ця функція використовує розмір блоку в 32 байти (256 байт) для вкладки вхідних даних, але AES використовує розмір блоку 128 біт. У AES256 ключ - 256 біт, але не розмір блоку.
Таннін

13
Іншим чином, "self.bs" слід видалити та замінити на "AES.block_size"
Олексій

2
Чому ти клюєш ключ? Якщо ви очікуєте, що це щось на зразок пароля, тоді вам не слід використовувати SHA256; краще використовувати ключову функцію деривації, як PBKDF2, яку надає PyCrypto.
tweaksp

5
@Chris - SHA256 видає 32-байтний хеш - ключ ідеального розміру для AES256. Створення / виведення ключа вважається випадковим / захищеним і повинно виходити за межі коду шифрування / дешифрування - хешування - це лише гарантія того, що ключ може бути використаний із вибраним шифром.
zwer

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:])]на неї, щоб вона працювала в різних версіях.
Тортується

2
@Torxed функція колодки сл в CryptoUtil.Padding.pad () з pycryptodome (PyCrypto катамнестического)
Конте

2
Чому б просто не мати константу символів, як накладки?
Inaimathi

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 для отримання ключа, але це, як правило, вважається поганою криптографічною практикою . Перегляньте wikipedia, щоб дізнатися більше про режими роботи.


iv_int = int (binascii.hexlify (iv), 16) не працює, замініть його на iv_int = int (binascii.hexlify (iv), 16) плюс 'імпорт binascii', і він повинен працювати (на Python 3.x ), інакше чудова робота!
Валмонд

Зауважте, що краще використовувати режими аутентифікованого шифрування як AES-GCM. GCM використовує внутрішньо режим CTR.
келалака

Цей код викликає "TypeError: Тип об'єкта <class 'str'> не можна передавати в код C"
Da Woon Jung

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]він збереже свою безпеку до вказаної вами довжини.


13
Не слід використовувати хеш-функцію сімейства SHA для генерування ключа з пароля - див . Твір Коди Хейла на цю тему . Подумайте про використання реальної функції виведення ключа на зразок scrypt . (Реферат Кода Хейла був написаний перед публікацією сценарію.)
Бенджамін Баренблат

7
Для майбутніх читачів, якщо ви хочете отримати ключ із парольної фрази, шукайте PBKDF2. Досить просто використовувати у python ( pypi.python.org/pypi/pbkdf2 ). Якщо ви шукаєте хеш-паролі, проте, bcrypt - кращий варіант.
C Fairweather

6

Вдячний за інші відповіді, які надихнули, але не спрацювали для мене.

Витративши декілька годин , намагаючись з'ясувати , як це працює, я прийшов з реалізацією нижче з новітньої PyCryptodomex бібліотеки (це інша історія , як мені вдалося встановити його за проксі, на Windows, в virtualenv .. уф) ,

що працюють на Ваша реалізація, не забудьте записати кроки прошивки, кодування, шифрування (і навпаки). Ви повинні упакувати та розпакувати, пам’ятаючи про порядок.

імпорт бази64
імпортувати хеліб
з Cryptodome.Cipher import AES
з Cryptodome.Random імпорт get_random_bytes

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

шифрування def (необроблене):
    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 розшифрувати (enc):
    unpad = лямбда s: s [: - ord (s [-1:])]

    enc = base64.b64decode (enc)
    iv = enc [: AES.block_size]
    шифр = AES.new (__ key__, AES.MODE_CFB, iv)
    повернути unpad (base64.b64decode (cipher.decrypt (enc [AES.block_size:])). decode ('utf8'))

Дякую вам за функціональний приклад цього з гілками 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()

5

Інший взяти на себе це (в значній мірі виходить з рішень вище), але

  • використовує null для оббивки
  • не використовує лямбда (ніколи не був фанатом)
  • тестовано на 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')

Це не спрацює, якщо у вхідному байті [] є кінцеві нулі, оскільки в функції decrypt () ви будете з'їдати ваші додаткові нулі PLUS будь-які зворотні нулі.
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 прокладки. Ви можете використовувати його замість попередніх функцій для прокладки (коли шифрування) та розблокування (коли дешифрування) .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 (), і він працює як шарм!
А СТЕФАНІ
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.