谈谈微信公众号开发中的信息加密(下)

上一篇说了微信消息的加密。但是微信公众号开发中需要密码学的地方并不止这些。

试想这样一个场景:用户通过 URL 查询所在位置附近的业余无线电中继台(或者饭店、旅馆、加油站),需要在 GET 报文中把自己的精确地理坐标(精确到10米吧)以 query string 的形式发送给服务器,例如这样:

http://repeater.applinzi.com/search/?lat=33.1234&lng=120.4321

精确的地理坐标属于较为敏感的用户信息,如果这样直接展示在 URL 里,那之前给消息加密不就成了掩耳盗铃?所以,需要加密(encryption)

我需要确保地理坐标在传输过程中没有被篡改,即完整性(integrity)

同时,我不希望用户把上面的 URL 地址改吧改吧自己编制一条 GET 报文来套取数据,所有合法的查询 URL 只能由我自己生成,这就需要认证(authentication)

某些场合,比如订单系统,还需要加密系统能抵御重放攻击(reply attack),然而我并不在乎用户点击同一条 URL 多次,所以这不在我的设计需求之内。

虽然 PyCrypto 分别提供了 Encryption 和 Authentication 算法,但是却并没有提供 Authenticated Encryption 的完整实现,需要用户自己把二者结合起来。结合方法有三种:

  • Encrypt-and-MAC,把明文分别加密和认证,然而认证算法是不一定能保证信息的私密性的,所以可能会导致明文信息泄露,不应该使用。
  • Encrypt-then-MAC,先把明文加密,然后对密文进行认证。推荐使用。
  • MAC-then-Encrypt,先对明文进行认证,然后把密文和签名一起进行加密。在某些情况下能保证安全,某些情况下不能。谨慎使用(其实就是别用)。

我还是使用了较为稳妥的 Encrypt-then-MAC。代码如下:

# utils/authencrypt.py

import base64
from Crypto.Cipher import AES
from Crypto import Random
from Crypto.Hash import HMAC


# 加密/认证算法只接受字符串,如果是unicode需要先转换
def to_str(text):
    if isinstance(text, unicode):
        text = text.encode('utf-8')
    return text


class AuthenticationError(Exception): pass


class AESCipher(object):
    def __init__(self, key):
        self.key = key

    def encrypt(self, plaintext):
        plaintext = to_str(plaintext)
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CFB, iv)
        return base64.b64encode(iv+cipher.encrypt(plaintext))

    def decrypt(self, ciphertext):
        ciphertext = base64.b64decode(ciphertext)
        iv = ciphertext[:16]
        cipher = AES.new(self.key, AES.MODE_CFB, iv)
        return cipher.decrypt(ciphertext[16:]).decode('utf-8')


class Authen(object):
    def __init__(self, key):
        self.key = to_str(key)

    def get_signature(self, text):
        text = to_str(text)
        hmac = HMAC.new(self.key)
        hmac.update(text)
        return base64.b64encode(hmac.digest())

    def authenticated(self, text, signature):
        return self.get_signature(text) == signature


class AE(object):
    def __init__(self, key):
        self.aes = AESCipher(key)
        self.authen = Authen(key)

    def encrypt(self, plaintext):
        ciphertext = self.aes.encrypt(plaintext)
        signature = self.authen.get_signature(ciphertext)
        return ciphertext, signature

    def decrypt(self, ciphertext, signature):
        if not self.authen.authenticated(ciphertext, signature):
            raise AuthenticationError
        return self.aes.decrypt(ciphertext)

下面是使用范例:

>>> from utils.authencrypt import AE
>>> key='1234567890zxcvbn'
>>> ae=AE(key)
>>> plain='attack at dawn'
>>> cipher, signature=ae.encrypt(plain)
>>> cipher
'C8qh7rZ0lKXvenjG4JOOHRnuO0MiWtQyNXfsZV2G'
>>> signature
'Hpx0c5zpe7GUQPczePjF7g=='
>>> decrypted=ae.decrypt(cipher, signature)
>>> decrypted
u'attack at dawn'

这样,文章开头的 URL 被转化为如下的形式:

http://repeater.applinzi.com/search/?cipher=C8qh7rZ0lKXvenjG4JOOHRnuO0MiWtQyNXfsZV2G&signature=Hpx0c5zpe7GUQPczePjF7g==

服务器接收到 GET 请求后,先验证签名,签名正确再解密,并利用解析出的地理坐标返回相应的结果。注意,不管其间出现了怎样的异常,如签名错误、解析不出坐标还是解析出的坐标没有对应的结果,都应该返回统一的错误提示,以防止攻击。

你可能感兴趣的:(谈谈微信公众号开发中的信息加密(下))