上一篇說了微信消息的加密卓研。但是微信公眾號開發(fā)中需要密碼學(xué)的地方并不止這些盏檐。
試想這樣一個場景:用戶通過 URL 查詢所在位置附近的業(yè)余無線電中繼臺(或者飯店、旅館芯咧、加油站)牙捉,需要在 GET 報文中把自己的精確地理坐標(biāo)(精確到10米吧)以 query string 的形式發(fā)送給服務(wù)器,例如這樣:
http://repeater.applinzi.com/search/?lat=33.1234&lng=120.4321
精確的地理坐標(biāo)屬于較為敏感的用戶信息敬飒,如果這樣直接展示在 URL 里邪铲,那之前給消息加密不就成了掩耳盜鈴?所以无拗,需要加密(encryption)带到。
我需要確保地理坐標(biāo)在傳輸過程中沒有被篡改,即完整性(integrity)英染。
同時揽惹,我不希望用戶把上面的 URL 地址改吧改吧自己編制一條 GET 報文來套取數(shù)據(jù),所有合法的查詢 URL 只能由我自己生成四康,這就需要認(rèn)證(authentication)搪搏。
某些場合,比如訂單系統(tǒng)闪金,還需要加密系統(tǒng)能抵御重放攻擊(reply attack)疯溺,然而我并不在乎用戶點擊同一條 URL 多次,所以這不在我的設(shè)計需求之內(nèi)哎垦。
雖然 PyCrypto 分別提供了 Encryption 和 Authentication 算法囱嫩,但是卻并沒有提供 Authenticated Encryption 的完整實現(xiàn),需要用戶自己把二者結(jié)合起來漏设。結(jié)合方法有三種:
- Encrypt-and-MAC墨闲,把明文分別加密和認(rèn)證,然而認(rèn)證算法是不一定能保證信息的私密性的愿题,所以可能會導(dǎo)致明文信息泄露损俭,不應(yīng)該使用。
- Encrypt-then-MAC潘酗,先把明文加密杆兵,然后對密文進(jìn)行認(rèn)證。推薦使用仔夺。
- MAC-then-Encrypt琐脏,先對明文進(jìn)行認(rèn)證,然后把密文和簽名一起進(jìn)行加密。在某些情況下能保證安全日裙,某些情況下不能吹艇。謹(jǐn)慎使用(其實就是別用)。
我還是使用了較為穩(wěn)妥的 Encrypt-then-MAC昂拂。代碼如下:
# utils/authencrypt.py
import base64
from Crypto.Cipher import AES
from Crypto import Random
from Crypto.Hash import HMAC
# 加密/認(rèn)證算法只接受字符串受神,如果是unicode需要先轉(zhuǎn)換
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 被轉(zhuǎn)化為如下的形式:
服務(wù)器接收到 GET 請求后格侯,先驗證簽名鼻听,簽名正確再解密,并利用解析出的地理坐標(biāo)返回相應(yīng)的結(jié)果联四。注意撑碴,不管其間出現(xiàn)了怎樣的異常,如簽名錯誤朝墩、解析不出坐標(biāo)還是解析出的坐標(biāo)沒有對應(yīng)的結(jié)果醉拓,都應(yīng)該返回統(tǒng)一的錯誤提示,以防止攻擊收苏。