BLOG
Enjoy when you can, and endure when you must.
JUL 21, 2015/后端开发与架构
数据加解密之 AES 篇

概念:对称加密算法

所谓对称加密,就是发送方和接收方对于一个数据采用同一密钥、同一加密方式对数据进行加解密的算法。在对称加密算法中,数据发送方将明文和加密密钥一起经过特殊加密算法处理后,生成复杂的加密密文进行发送;数据接收方在收到密文后,则使用加密使用的密钥及相同算法的逆算法对加密的密文进行解密,从而使其恢复成可读明文。在对称加密算法中,使用的密钥只有一个,发送方和接收方都使用这个密钥对数据进行加密和解密,这就要求加密和解密方实现都需知道加密的密钥。

对称加密算法的特点是算法公开、计算量小、加密速度快、加密效率高。但因为双方强依赖于同一加密密钥,因此密钥本身对于安全性来说至关重要。

常用的对称加密算法包括 DES、3DES 以及 AES 等。其中 AES 算法相对先进,是对称加密算法中最流行的算法之一。因此也是这里关注的重点所在。

AES 在 Python 中的应用与分解

在 Python 中运用 AES 加密是很简单的,只需安装一个 PyCrypto 加密算法库即可快速展开工作。

加密和解密的第一步在于得到一个 AESCipher 对象,只需通过下面的方法即可:

from Crypto.Cipher import AES

cipher = AES.new(key, mode, iv)

该方法返回一个 AESCipher 对象。先来看一下其主要接收的三个参数:

  • key:初始密钥。根据 AES 规范,可以是 16 字节、24 字节和32 字节长,分别对应 128 位、192 位和 256 位;
  • mode:加密模式。可以寻找相关的文档了解。接下来的示例中会用到 CBC 模式,因此在此作一个简单的阐述:CBC 模式是先将明文切分成若干小段,然后每一小段与初始块或者上一段的密文段进行异或运算后,再与密钥进行加密;
  • vi:初始化向量。在部分加密模式中需要使用到,如对于 CBC 模式来说,它必须是随机选取并且需要保密的;而且它的长度和密码分组相同(AES 的分组长度固定为 16 字节)。

在加密之前,还需对明文进行一次处理。因为 AES 内部始终使用 16 字节的分组长度。加密时,如果明文字节长度不是 16 的整数倍,则需要通过如 ZeroPadding、PKCS#7 等填充方式对其进行补位填充。以 PKCS#7 为例:

PKCS#7 填充字符串由一个字节序列组成,每个字节填充该字节序列的长度。

如假定块长度为 8,数据长度为 9,则填充用八位字节数等于 7。如果数据长度刚好为块长度的倍数,则添加一个完整块长度的填充位。例如数据等于 FF FF FF FF FF FF FF FF FF,则:

数据: FF FF FF FF FF FF FF FF FF

PKCS#7 填充: FF FF FF FF FF FF FF FF FF 07 07 07 07 07 07 07

准备工作就绪后,接下来即可对数据进行加密操作:

cipher_text = cryptor.encrypt(text)

为便于传输,一般对加密后的数据进行 base64 编码:

encoded = base64.b64encode(cipher_text)

这里的 encoded 即为加密后的数据。

理解了加密方法及相关的概念之后,解密则很简单,只需使用相同的密钥和模式进行解密操作即可。一个完整的简单示例如下所示:

>>> from Crypto.Cipher import AES
>>> text = 'qwertyuiopasdfgh'

>>> import random, string
>>> key = ''.join(random.sample(string.ascii_letters + string.digits, 32))
>>> key
'L9qeRO25ojyXv37aWwrKpSAPYzVg6kBT'

>>> cipher = AES.new(key, AES.MODE_CBC, key[:16])
>>> cipher_text = cipher.encrypt(text)

>>> import base64
>>> encoded = base64.b64encode(cipher_text)
>>> encoded
'PqQmoF4WcBTJ+AfMJrYjpA=='

>>> cipher = AES.new(key, AES.MODE_CBC, key[:16])
>>> cipher_text = base64.b64decode(encoded)
>>> text = cipher.decrypt(cipher_text)
>>> text
'qwertyuiopasdfgh'

用实例说话:微信公众平台开发中的消息加解密

微信的公众平台在曾经的某个时候引入了“安全模式”,即由微信主动向第三方平台发起的请求(被动响应请求)的消息体会进行加密,而加密的算法采用的正是 AES。在之前的示例中可以看出,实际加解密都是封装好的方法,只需调用 AESCipher 对象的 encrypt 和 decrypt 方法即可。所以其实在实际应用中,真正关心的主要是准备阶段,即 key 的生成和对所要加密的明文的处理方式。

微信公众平台的密钥来源于 EncodingAESKey,其长度为 43 个字符,使用时在其最后加一个“=”,然后用 base64 对其解码即可还原得到 AES 密钥,长度为 32 个字节:

key = base64.b64decode(self.encoding_aes_key + '=')

然后是加密模式,微信公众平台的 AES 采用的是 CBC 模式,该模式需要一个初始化向量,从代码中可以看出是 key 的前 16 个字节:

cryptor = AES.new(self.key, self.mode, self.key[:16])

最后要关注的就是对待加密内容的处理:首先整个内容由 16 个字节的随机字符串、4 个字节的 msg_len(网络字节序)、msg 和 $AppId 组成,其中 msg_len 为 msg 的长度,$AppId 为公众帐号的 AppId。然后数据采用 PKCS#7 填充:

在数据的尾部填充 (K - N % K) 个字节,每个字节的内容是 (K - N % K)

其中 K 为秘钥字节数(采用 32),N 为待加密内容的字节数。

来看看相关的代码:

class PKCS7Encoder(object):
    """
    提供基于PKCS7算法的加解密接口
    """

    block_size = 32

    def encode(self, text):
        """
        对需要加密的明文进行填充补位
        @param text: 需要进行填充补位操作的明文
        @return: 补齐明文字符串
        """

        text_length = len(text)

        # 计算需要填充的位数
        amount_to_pad = self.block_size - (text_length % self.block_size)
        amount_to_pad = self.block_size if amount_to_pad == 0 else amount_to_pad

        # 获得补位所用的字符
        pad = chr(amount_to_pad)

        return text + pad * amount_to_pad

    def decode(self, decrypted):
        ...

# 16位随机字符串添加到明文开头
text = text.encode('utf-8')
appid = appid.encode('utf-8')
text = ''.join([self.get_random_str(), struct.pack('I', socket.htonl(len(text))), text, appid])

# 使用自定义的填充方式对明文进行补位填充
text = PKCS7Encoder().encode(text)

以上仅仅是对于 AES 加密的应用,其真正的加密原理也值得我们去深入研究一下,才能帮助我们更好的去理解它。

COMMENTS
LEAVE COMMNT