早在2013年,我就设计了 开放平台,那时参考了 新浪开放平台 \腾讯\百度\淘宝\支付宝\豆瓣 开放平台,并研究了 OAuth(1.0和2.0)最终第一版采用的 OAuth 1.0实现,第二版采用 OAuth 2.0 实现。但是有一个疑问,当时没弄清楚,就是 “为什么支付宝用的 RSA 加密和签名,而新浪、豆瓣等用的 AES 加密、SHA1 签名?”
现在,我又深入研究了一晚上,终于想明白了,下面从头说起。
关于加密算法
只谈 AES 算法和 RSA 算法,其他的都不讨论,比如 DES,已经过时了。
问:AES和RSA算法的区别是什么,哪个安全性更高?
AES属于 对称加密算法,加密和解密用的密钥是一有的。 而RSA属于非对称加密算,加密和解密过程使用不同的密钥。公钥即为公开的密钥,一般用作加密;私钥即为私有的密钥,一般用作解密。
从安全性角度比较,以目前的科技水平来看,RSA和AES都很难破解,可以认为是足够安全的。从算法角度来比较,AES 256 的安全性比 RSA 1024 的安全性还是要高得多,而且AES算法的计算速度比RSA要快得多。RSA 太慢了,以至于不适合对比较大的数据进行加解密。但从应用角度来看,RSA和AES各有各的用途,AES要向使用者公开密匙,是有很大安全隐患的。而RSA则只需要对使用者公开公钥,私钥不对外公开,但是由于RSA运算速度很慢,所以一般只用于签名等数据很少的情况。
AES算法,常用的两种模式:CBC 和 GCM。据说从理论角度来看,GCM更有优势,应该是未来的主流趋势。而现在的主流,是CBC模式。
亚马逊云 AWS,默认采用的就是 AES_256_GCM(全称: ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384),安全性非常高。但是只提供了 Java 和 Python的 client,而且我没搜到有其他语言的第三方实现。而 AES_256_CBC,应用更为广泛,微信开放平台就是用的它,而且提供了 Java、PHP、Python、C#、C++ 五种语言的官方实现,而且第三方的实现还包括 Go、NodeJS 等。总之,这个算法实现较容易,各个语言对它的支持非常广,使用方便。
综上,我目前还是推荐 AES_256_CBC,我认为强度足够了,关键是方便。
AES算法,是对称加密算法,加密、解密共用一个密匙。而 RSA 算法,是非对称加密算法,它有一对密匙:公钥用于加密、验签,私钥用于解密、加签。所以,RSA 算法用于加解密时,可以把公钥直接公布出来,对方拿去加密,而私钥只有一份在自己手上,只有自己能解密,这个特性非常有用!本文后面会看到。
然而,遗憾的是,RSA 算法只能用于很短的数据加密,以 1024 位 key 为例,最多只能加密 127 位数据。
关于签名算法
常见的签名算法是 MD5、SHA1、SHA256,其他的本文不谈。所谓签名算法,就是可以根据数据,计算出一个长度固定的内容,只要数据有任何改变,算出来的值都不一样。
MD5算法安全性较弱,比较容易被暴力破解。但是 MD5 签名的速度很快,性能好。可以用于不是特别注重安全性的场景。SHA1,可以作为 MD5 的替代者,它的性能也不错,而且几乎不会被破解。而 SHA256,强度更高。
问题:消息摘要(MD-Message Digest,例如 MD5、SHA256等)和消息认证(MA-Message Authentication,例如HMAC-MD5、HMAC-SHA1、HMAC-SHA256等)的区别是什么?
MD 是用来防篡改的,而 MA 是用来核对身份的。比如一个镜像文件,计算其 MD5 值,如果相同则没有被修改。但是如果进一步要求,这个镜像文件必须是从A网站下载的,这是就要使用MA进行认证,使用者根据 key 计算 MA 值,认证通过后就可以确认这个文件是从A网站下载的,而且没有被篡改。再比如你和对方共享了一个密钥 K,现在你要发消息给对方,既要保证消息没有被篡改,又要能证明信息确实是你本人发的,那么就把原信息和使用 K 计算的 HMAC 的值一起发过去。对方接到之后,使用自己手中的K把消息计算一下HMAC,如果和你发送的HMAC一致,那么可以认为这个消息既没有被篡改也没有冒充。
签名和加密结合的算法
常用的是 SHA1WithRSA、SHA256WithRSA,即 用 SHA 算法进行签名,再用 RSA 算法进行加密。最终得到的一个密文的签名。
常见应用案例 - 加密加签、防重放攻击
AES 和 RSA 组合使用,发挥各自的特点
为了避免 AES Key 在网络上明文传输,先动态生成 AES 的 key,然后对数据进行 AES 加密。然后用 RSA 公钥加密 AES Key。最后将加密的数据和 aesKey 一起传给接收方。接收方用 RSA 私钥解密 AES Key,然后用解密后的 AES Key 对数据进行 AES 解密。
AES 和 SHA 组合使用,发挥各自的特点
与上面的 RSA 有所不同,但是利用 RSA 有异曲同工之妙。方法是,双方都保存同一个 token(可以理解为密码),然后用 token 和所有参数一起利用 SHA 算法计算一个签名,接收方利用自己本地的相同的 token 和接收到的参数一起也计算一个签名,签名一致则代表数据没有被修改。为保证数据不明文传输,也可以用 AES 进行加密,但是 AES 的密匙需要事前配置好,双方都用同一个 AES Key。
综上,不难看出这两种主流方式的区别。应该说特点不一样,各有优劣。AES+RSA 方案,只需要客户端配置一个 RSA 公钥即可,用于加密 AES Key。而 AES+SHA 方案,需要客户端配置 aesKey 和 token。并且注意到,后者连签名一起做了,而前者只有加密,没有签名,如果要签名,则还需要引入 SHA1-RSA 算法,再提供一对公钥密钥。
两个方案的具体分析以及如何防重放攻击
AES和RSA:【发送方】操作步骤
1. 随机生成AES密匙(aesKey),用以对明文数据(clearData)加密,加密后的数据为encryptData。
2. 用接收方提供的RSA公钥(receiverPublicRsaKey),对AES密匙进行加密,得到encryptAesKey。
3. 用自己的RSA私钥(senderPrivateRsaKey),对密文数据(encryptData+encryptAesKey)进行签名,得到sign。
关键代码如下:
public byte[] encryptAndSign( byte[] clearData, String encryptType, byte[] receiverPublicRsaKey, String signType, byte[] senderPrivateRsaKey) {
if (null != receiverPublicRsaKey) {
// 加密
}
if (null != senderPrivateRsaKey) {
// 加签
}
return clearData;
}
AES 和 RSA:【接收方】操作步骤
- 用发送方提供的RSA公钥(senderPublicRsaKey),对接收密文数据(encryptData+encryptAesKey)进行验签,验签失败则结束。
- 用自己的RSA私钥(receiverPrivateRsaKey),对encryptAesKey进行RSA解密,得到aesKey。
- 用aesKey对密文数据encryptData进行AES解密,得到明文数据clearData。
关键代码如下:
public byte[] checkSignAndDecrypt(byte[] data, String signType, byte[] senderPublicRsaKey, byte[] sign, String encryptType, byte[] receiverPrivateRsaKey) {
if (signType != null) {
// 验签
}
if (encryptType != null) {
// 解密
}
return null;
}
关键之处: 应用端(接收方),要向服务端(发送方)提供 自己的 appId 和 RSA 解密公钥。 并且,要保存服务端(发送方)提供的 RSA 验签公钥。为方便和易扩展,服务端(发送方)可以选择是否对数据进行加密和加签,并且从数据上就可以判断,是否进行了加密和加签,以及加密和加签的类型。所以,一个完整的数据组成如下(以JSON为例):
{data:"...", encryptType:"AES...", sign:"...", signType:"RSA..."}
其中,encryptType、sign 和 signType,都是非必须的。注意到另外一种流行的签名方式:msg_signature=sha1(sort(Token、timestamp、nonce, msg_encrypt))
这种签名方式的好处在于,它使用 SHA1 算法,这个算法和 MD5 类似,是公开的,不需要密匙。 另外注意,此处的 token,其实是一个密码,是应用端(接收方)配置的,和 appid 一起配置的,服务端(发送方)也知道这个密码,所以这个密码(Token)不会在网络上传输。这样一来,黑客即使知道算法和传递的参数,但他不知道密码(Token),所以他生成的 signature,在服务端无法通过校验。
另外,之所以加上 timestamp 和 nonce,是为了能够防止重放攻击(Replay-Attack),但为此还得做特殊处理:可选的实现方式是把每一次请求的 Nonce 保存到数据库,客户端再一次提交请求时将请求头中得 Nonce 与数据库中得数据作比较,如果已存在该 Nonce,则证明该请求有可能是恶意的。然而这种解决方案也有个问题,很有可能在两次正常的资源请求中,产生的随机数是一样的,这样就造成正常的请求也被当成了攻击,随着数据库中保存的随机数不断增多,这个问题就会变得很明显。所以,还需要加上另外一个参数 Timestamp(时间戳)。之所以把 timestamp 和 nonce 一并做 SHA1,是防止被修改,假如他修改了 timestamp,他又没办法生成有效的 signature,所以无法通过校验。
问题又来了,随着用户访问的增加,数据库中保存的 nonce/timestamp/appid 数据量会变得非常大。对于这个问题,可选的解决方案是对数据设定一个“过期时间”,比如说在数据库中保存超过一天的数据将会被清除。如果是这样的,攻击者可以等待一天后,再将拦截到的 HTTP 报文提交到服务器,这时候因为 nonce/timestamp/appid 数据已被服务器清除,请求将会被认为是有效的。
要解决这个问题,就需要给时间戳设置一个超时时间,比如说将时间戳与服务器当前时间比较,如果相差一天则认为该时间戳是无效的。
对比 RSA 签名方案,他们其实都要保存密码,RSA方案服务端(发送方)需要保存一个自己的 RSA 私钥以及所有应用端(接收方)的 RSA 公钥,而 SHA1 方案,服务端(发送方)需要保存所有应用端(接收方)的 token。但是,SHA1 速度更快,RSA验签其实是先把消息进行 SHA1 或者 SHA256(以便控制长度,RSA 加解密对长度有限制),然后再 RSA 解密,即 SHA1WithRSA、SHA256WithRSA 算法,故速度稍慢。
说到防重放攻击,RSA 方案也是可以的,在 RSA 参数中,加上 nonce 和 timestamp,并且一起做签名。黑客无法篡改 nonce 和 timestamp,但是又只能一次性使用,那就达到了防重放的目的。
总的来说,RSA 方案缺点明显:担心私钥泄露,而且私钥改了会影响所有客户端数据签名,其优点就是不用保存客户端的密钥。个人建议使用 SHA 方案,配置 aesKey 用于加密,配置 token 配合 SHA 算法用于数据校验。实际上微信开放平台就是用的这个方案。