上一篇说了微信消息的加密。但是微信公众号开发中需要密码学的地方并不止这些。
试想这样一个场景:用户通过 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 被转化为如下的形式:
服务器接收到 GET 请求后,先验证签名,签名正确再解密,并利用解析出的地理坐标返回相应的结果。注意,不管其间出现了怎样的异常,如签名错误、解析不出坐标还是解析出的坐标没有对应的结果,都应该返回统一的错误提示,以防止攻击。