使用PyCrypto AES-256进行加密和解密

212

我正在尝试使用PyCrypto构建两个函数,接受两个参数:消息和密钥,然后加密/解密消息。

我在网上找到了几个链接来帮助我,但它们每一个都有缺陷:

codekoala的这个使用 os.urandom,但是PyCrypto不鼓励这样做。

此外,我给函数的密钥的长度不能保证精确。我该怎么办才能让它符合要求?

还有几种模式可用,哪一种推荐使用?我不知道该用什么 :/

最后,IV是什么? 我可以为加密和解密提供不同的IV吗,或者这会导致结果不同?


15
PyCrypto网站提倡使用os.urandom,它使用微软的CryptGenRandom函数,该函数是一个CSPRNG - Joel Vroom
5
在Unix系统中,可以使用/dev/random/dev/urandom来获取随机数据。 - Joel Vroom
2
只是为了澄清,在这个例子中,passphrase 是作为 key 使用的,它可以是 128、192 或者 256 位(即 16、24 或者 32 字节)。 - Mark
13
值得一提的是,PyCrypto是一个已经停止维护的项目(https://github.com/dlitz/pycrypto/issues/173)。最后一次提交是在2014年。 PyCryptodome看起来是一个很好的可替换选项。 - Overdrivr
5
这个问题比较陈旧,但是我想指出(截至2020年)pycrypto可能已经过时并且不再受支持。从他们的Github页面(https://github.com/pycrypto/pycrypto)看,它们的最后一次提交是在2014年。我会对使用不再维护的加密软件感到警惕。 - irritable_phd_syndrome
显示剩余5条评论
16个回答

214
这是我的实现,经过一些修复后对我有效。它通过将密钥和秘密短语的对齐增强到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 AESCipher._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:])]

5
你为什么要对密钥进行哈希?如果你期望它像密码一样安全,那就不应该使用SHA256,最好使用密钥派生函数,例如PyCrypto提供的PBKDF2。 - tweaksp
6
@Chris - SHA256 生成一个32字节的哈希值,这是用于AES256的完美密钥大小。假定密钥的生成/派生是随机/安全的,并且应该超出加密/解密代码的范围-哈希只是保证密钥与所选密码可以使用的担保。 - zwer
2
@mnothic,为什么_pad是普通的类方法而_unpadstaticmethod?它们之间有什么区别? - Boffin
2
在 _pad 中需要访问 self.bs,而在 _unpad 中不需要。 - mnothic
3
@mnothic - 一个愚蠢的问题:为什么要使用s[:-ord(s[len(s)-1:])] 而不是 s[:-ord(s[-1])] - ryugie
显示剩余13条评论

158

你可能需要以下两个函数: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:])]

所以你在问密钥的长度?你可以使用密钥的 MD5 哈希值来代替直接使用它。

此外,根据我使用 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,然后会将长度为BLOCK_SIZE的chr(BS)值序列附加到原始数据中。 - Marcus
2
@Marcus,“pad”函数已经失效(至少在Py3中),请使用s[:-ord(s[len(s)-1:])]替换以使其跨版本工作。 - Torxed
2
@Torxed pad函数在CryptoUtil.Padding.pad()中可用,使用pycryptodome(pycrypto的后继版本)。 - comte
2
为什么不将字符常量作为填充字符呢? - Inaimathi
显示剩余10条评论

22
让我来回答你关于“模式”的问题。AES-256是一种分组密码。它以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。我建议在使用PyCrypto时谨慎使用AES-CBC。原因是它要求您指定填充方案,就像其他解决方案所示。一般来说,如果您对填充不是非常小心,就会有攻击完全破解加密!
现在,重要的是要注意,密钥必须是一个随机的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)

一些解决方案建议使用SHA-256来派生密钥,但这通常被认为是不良的加密实践查看维基百科以了解更多操作模式。

iv_int = int(binascii.hexlify(iv), 16) 这段代码无法正常工作,将其替换为 iv_int = int(binascii.hexlify(iv), 16) 并加上 'import binascii' 应该就可以在 Python 3.x 上正常运行了。否则,工作得很出色! - Valmond
请注意,最好使用经过身份验证的加密模式,如AES-GCM。GCM内部使用CTR模式。 - kelalaka
此代码导致“TypeError: 无法将对象类型<class 'str'>传递给C代码”。 - Da Woon Jung
@DaWoonJung 我在Python 3.9.7中也观察到了同样的情况。_ciphertext = aes.encrypt(plaintext.encode('utf-8'))_,而_ciphertext_需要使用_decode('utf-8')_来完成str、bytes的往返转换。 - Stephan Scheller
当我尝试解密时,出现了错误。错误信息是:“'bytes' object has no attribute 'encode'”。但是当我检查iv的类型时,它显示为<class 'bytes'>。如何解决这个问题? - HelpMeInDjango

10
我很感激其他答案的启发,但对我没有用。
在花费数小时试图弄清楚它如何工作后,我使用最新的PyCryptodomex库编写了下面的实现(关于如何在代理后面,在Windows中,在virtualenv中设置它的故事... 哎呀)。
它在您的实现上运行。记住编写填充、编码和加密步骤(以及反之亦然)。您必须打包和解包,记住顺序。
import base64
import hashlib
from Cryptodome.Cipher import AES
from Cryptodome.Random import get_random_bytes

__key__ = hashlib.sha256(b'16-character key').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(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'))

非常感谢您提供PyCryptodomeX库的功能示例。那非常有帮助! - Ygramul

8

如果你想使用urlsafe_b64encode和urlsafe_b64decode,这里是我使用的版本(在处理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

您可以通过使用密码学哈希函数(不是 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]来截断密码哈希值,并且它将保持其安全性,直到您指定的长度。


15
从密码生成密钥时,不应使用SHA系列哈希函数-请参阅Coda Hale关于此主题的文章。 考虑改用真正的密钥派生函数,例如scrypt。 (Coda Hale的文章是在scrypt发布之前撰写的。) - Benjamin Barenblat
7
对于未来的读者,如果您想从密码短语中派生密钥,请使用PBKDF2算法。这个算法在Python中使用相当容易(https://pypi.python.org/pypi/pbkdf2)。然而,如果您想要对密码进行哈希处理,则bcrypt是更好的选择。 - C Fairweather

6
另一种看法(大量参考以上解决方案),但是:
  • uses null for padding
  • does not use lambda (never been a fan)
  • tested with python 2.7 and 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')
    

如果输入的byte[]具有尾随的null,则此方法将无法正常工作,因为在decrypt()函数中,您将吞掉填充的null以及任何尾随的null。 - Buzz Moschetti
是的,正如我上面所述,此逻辑使用 null 进行填充。如果您要编码/解码的项可能具有尾随的 null 值,则最好使用这里的其他解决方案之一。 - MIkee
警告:此函数不支持拉丁字符和特殊字符。 - Elias Prado

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

嗨,我是这个领域的新手。你能告诉我在“key”中传递什么吗? - Harshit verma

5

我曾经使用过 CryptoPyCryptodomex 库,它们的速度非常快...

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)

类型错误:'iv' 是此函数的无效关键字参数。出现错误。 - Malavan P

3
您可以使用像PKCS#7填充这样的方案。您可以在加密时使用它来代替以前的填充函数,并在解密时使用它来取消填充。我将在下面提供完整的源代码。
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())

我不知道是谁给答案点了踩,但我很想知道为什么。也许这个方法不安全?能否请解释一下?谢谢。 - Cyril N.
1
@CyrilN。这个答案建议使用单次SHA-256哈希密码就足够了,但实际上并不是这样的。您真的应该使用PBKDF2或类似算法来从密码中进行密钥派生,并使用大量迭代次数。 - Artjom B.
我有一个长度为44的密钥和iv密钥。我该如何使用你的函数?我在互联网上找到的所有算法都存在我的向量密钥长度问题。 - mahshid.r

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接