使用PyCrypto AES-256加密和解密

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

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

这个在codekoala使用os。这是PyCrypto不鼓励的。

此外,我给函数的键不能保证具有预期的确切长度。我要怎么做才能做到呢?

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

最后,4到底是什么?我是否可以提供不同的IV来加密和解密,或者这将返回不同的结果?

588848 次浏览

你可以通过使用加密哈希函数( 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]截断一个加密哈希值,它将保留其安全性直到你指定的长度。

你可能需要以下两个函数:pad-填充(加密时)和unpad- 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:] ))

为了其他人的利益,这里是我的解密实现,我通过组合@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()

下面是我的实现,它通过一些修复为我工作。它将密钥和秘密短语的对齐增强为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:])]

对于想要使用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:]))
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:])

您可以使用类似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())

这是另一种观点(主要源自上述解决方案),但是

  • 使用null填充
  • 不使用lambda(从来不是粉丝)
  • 测试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')
    

让我来回答你关于“模式”的问题。aes - 256是一种分组密码。它接受一个32字节的关键和一个称为的16字节字符串作为输入,并输出一个块。为了加密,我们在操作模式中使用AES。上面的解决方案建议使用加拿大广播公司,这是一个例子。另一个被称为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)
上面的一些解决方案建议使用sha - 256来派生键,但这通常被认为是糟糕的密码实践查阅维基百科获取更多关于操作模式的信息

我很感激其他启发我的答案,但它们对我不起作用。

在花了几个小时试图弄清楚它是如何工作的之后,我用最新的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'))

看到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

我已经使用了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)

使用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,它将不接受这种类型的数据。你也需要在那里换衣服。

你可以使用新的django-mirage-field包。

PyCrypto已经过时了。

密码学现在有更好的支持。

这是另一个实现。注意,这将返回字节,您需要使用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