使用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个回答

2

请查看mnothic的最初回答

UTF-8编码兼容:

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

这是什么?对那个答案的更正吗?还是其他什么?请详细说明。 - Peter Mortensen
好的,楼主已经离开了:"上次出现超过1年前"。 - Peter Mortensen

1

AES-256使用utf8mb4加密和解密拉丁字符和特殊字符(中文):

对于需要加密和解密拉丁字符和特殊值(例如中文)的用户,这里是对@MIkee代码进行修改以完成此任务。

请记住,仅使用UTF-8无法处理此类型的编码。

import base64, re
from Crypto.Cipher import AES
from Crypto import Random
from django.conf import settings

import codecs

# Make utf8mb4 recognizable.
codecs.register(lambda name: codecs.lookup('utf8') if name == 'utf8mb4' else None)


class AESCipher:

    def __init__(self, key, blk_sz):
        self.key = key
        self.blk_sz = blk_sz

    def encrypt( self, raw ):
        # raw is the main value
        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('utf8mb4')
        # Initialization vector to avoid same encrypt for same strings.
        iv = Random.new().read( AES.block_size )
        cipher = AES.new( self.key.encode('utf8mb4'), AES.MODE_CFB, iv )
        return base64.b64encode( iv + cipher.encrypt( raw ) ).decode('utf8mb4')

    def decrypt( self, enc ):
        # enc is the encrypted value
        if enc is None or len(enc) == 0:
            raise NameError("No value given to decrypt")
        enc = base64.b64decode(enc)
        iv = enc[:16]
        # AES.MODE_CFB that allows bigger length or Latin values
        cipher = AES.new(self.key.encode('utf8mb4'), AES.MODE_CFB, iv )
        return re.sub(b'\x00*$', b'', cipher.decrypt( enc[16:])).decode('utf8mb4')

使用方法:

>>> from django.conf import settings
>>> from aesencryption import AESCipher
>>>
>>> aes = AESCipher(settings.SECRET_KEY[:16], 32)
>>>
>>> value = aes.encrypt('漢字')
>>>
>>> value
'hnuRwBjwAHDp5X0DmMF3lWzbjR0r81WlW9MRrWukgQwTL0ZI88oQaWvMfBM+W87w9JtSTw=='
>>> dec_value = aes.decrypt(value)
>>> dec_value
'漢字'
>>>

相同的规则也适用于拉丁字母,例如 ã, á, à, â, ã, ç 等等。
注意事项
请记住,如果您将拉丁字符存储到数据库中,您需要设置允许此类数据的选项。因此,如果您的数据库设置为 utf-8,它将无法接受此类数据。您还需要在那里进行更改。

1

PyCrypto已经有些老旧了。

现在cryptography提供了更好的支持。

这里是另一个实现。请注意,它返回字节(bytes),你需要使用base64将它们转换为字符串以进行传输。

import os
import hashlib
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend

_BLOCK_SIZE = 16

class AesStringCipher:
    def __init__(self, key):
        self._key = hashlib.sha256(key.encode()).digest()

    def encrypt_str(self, raw:str) -> bytes:
        iv = os.urandom(_BLOCK_SIZE)
        cipher = Cipher(algorithms.AES(self._key), modes.CBC(iv), default_backend())
        encryptor = cipher.encryptor()
        raw = _pad(raw)
        return iv + encryptor.update(raw.encode('utf-8')) + encryptor.finalize()

    def decrypt_str(self, enc:bytes) -> str:
        iv = enc[:_BLOCK_SIZE]
        enc = enc[_BLOCK_SIZE:]
        cipher = Cipher(algorithms.AES(self._key), modes.CBC(iv), default_backend())
        decryptor = cipher.decryptor()
        raw = decryptor.update(enc) + decryptor.finalize()
        raw = raw.decode('utf-8')
        return _unpad(raw)

def _pad(s:str) -> str:
    padding = (_BLOCK_SIZE - (len(s) % _BLOCK_SIZE))
    return s + padding * chr(padding)

def _unpad(s:str) -> str:
    return s[:-ord(s[len(s)-1:])]


if __name__ == '__main__':
    cipher = AesStringCipher('my secret password')

    secret_msg = 'this is a super secret msg ...'
    enc_msg = cipher.encrypt_str(secret_msg)
    dec_msg = cipher.decrypt_str(enc_msg)

    assert secret_msg == dec_msg

关于“PyCrypto已经过时”的说法,是的,“这个软件已经不再维护。PyCrypto 2.x版本已经停止维护,已经过时,并且存在安全漏洞。” - Peter Mortensen

0
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:])

13
请提供代码并解释你正在做什么以及为什么这样做更好/与现有答案的区别是什么。 - Florian Koch
将 md5.new(key).digest() 替换为 md5(key).digest(),它会像魔法般工作! - A. STEFANI

0

它执行一些加密操作,但你能详细说明一下吗? - Peter Mortensen
是的,它可以“开箱即用”地加密字段。 - dontsov

0
根据@mnothic的实现,我对其进行了一些修改以修复一些问题。首先,我将_unpad改为实例方法,因为在decrypt中它是通过self.前缀而不是AESCipher前缀调用的。我所做的第二件事是改变填充操作顺序,以适应使用字符编码后结果超过一个字节的文本,从而移动填充位置。所以我所做的就是将填充放在编码之后。最后,我将填充和��填充逻辑更改为能够处理字节而不是字符串,并且还考虑了极端情况,即原始文本不需要填充的情况下,之前的逻辑会失败。
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):
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return base64.b64encode(iv + cipher.encrypt(self._pad(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) * bytes(0x01)

    def _unpad(self, s):
        for i in range(1, len(s) - 1):
            if s[-i] != 0:
                break
        i -= 1
        return s[:-i] if i > 0 else s

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