非对称密码加密解密&签名机制&重放攻击
《Internal APIs encrypt Method V1.0》 文章中已经描述了我们正在线上产品所用的加密算法,实际上他不仅仅是解决防参数篡改的问题,同时也解决了数据隐私保护的问题。即便如此这套算法还是有漏洞的,也是做所以定义为 Internal APIs的主要原因了,由于只提供给内部使用所以安全性暂且得到保障,一旦提供给外部使用则全盘”露馅”了,这也是我为什么要编辑此篇《Open APIs encrypt Method V1.1》文章了。
其中最为致命的问题是要在客户端代码中公开自己的密钥以及算法,一旦黑客从客户端程序中反编译源码,这样就会导致整个加密体系的崩盘,而且连补救的措施都没有。
同时第二个比较严重的问题就是黑客从放攻击的问题,黑客在抓取到包体后,直接再次提交请求导致服务器端不断受到重复请求。
所以基于以上两点,我们了解到《Internal APIs encrypt Method V1.0》所存在的问题如下表所示
问题描述 | 安全性 | 解决方案 |
---|---|---|
脱敏(数据隐私保护) | 安全 | |
完整性 (防参数篡改) | 安全 | |
重放攻击 (重复提交) | 未解决 | timestamp + nonce |
对称密钥 | 不够安全 | 非对称密钥 |
签名机制:
所以本文重点就是要解决 重放攻击 (重复提交) & 密钥安全 这两个问题。在此之前再介绍一种常用的解决数据传输过程中确保完整性 (防参数篡改)的解决方案,过程如下:
- 客户端使用约定好的秘钥对传输参数进行加密,得到签名值signature,并且将签名值也放入请求参数中,发送请求给服务端
- 服务端接收客户端的请求,然后使用约定好的秘钥对请求的参数(除了signature以外)再次进行签名,得到签名值autograph。
- 服务端对比signature和autograph的值,如果对比一致,认定为合法请求。如果对比不一致,说明参数被篡改,认定为非法请求。
Eg.客户端 zhangsan 执行登录请求
将要发送的数据:apihost?action=login&username=zhangsan&password=123456&sign=
签名算法:(java 实现)
1 | //1.对除sign外的所有参数按字典排序 对所有待签名参数按照字段名的 ASCII码从小到大排序(字典序)后 |
输出的sign为:sign=ebb36366bd9d8656e2327ca913a4f854f35a0e95
最终请求服务器的数据为:
apihost?action=login&username=zhangsan&password=123456&sign=ebb36366bd9d8656e2327ca913a4f854f35a0e95
当服务器拿到数据后再进行同样的算法然后再匹配sign值是否一致,如果对比一致,认定为合法请求。如果对比不一致,说明参数被篡改,认定为非法请求。
注意事项:
- 参数排序很重要,不同的排序会导致签名值不一致。所以要事先规定好排序算法。
- sign算法也要事先规定好,示例中是一个简单的算法并没有使用到密钥。实际过程中可能更加复杂签名的秘钥我们可以使用很多方案,可以采用对称加密或者非对称加密。
- 因为黑客不知道签名的秘钥,也不知道签名的算法,所以即使截取到请求数据,对请求参数进行篡改,但是却无法对参数进行签名,无法得到修改后参数的签名值signature。
- 示例中并没有解决数据脱敏的问题,应用过程中可以根据实际情况再进行脱敏算法。
防止重放攻击
基于timestamp的方案
每次HTTP请求,都需要加上timestamp参数,然后把timestamp和其他参数一起进行数字签名。因为一次正常的HTTP请求,从发出到达服务器一般都不会超过60s,所以服务器收到HTTP请求之后,首先判断时间戳参数与当前时间相比较,是否超过了60s,如果超过了则认为是非法的请求。
一般情况下,黑客从抓包重放请求耗时远远超过了60s,所以此时请求中的timestamp参数已经失效了。
如果黑客修改timestamp参数为当前的时间戳,则signature参数对应的数字签名就会失效,因为黑客不知道签名秘钥,没有办法生成新的数字签名。
但这种方式的漏洞也是显而易见的,如果在60s之后进行重放攻击,那就没办法了,所以这种方式不能保证请求仅一次有效。
基于nonce的方案
nonce的意思是仅一次有效的随机字符串,要求每次请求时,该参数要保证不同,所以该参数一般与时间戳有关,我们这里为了方便起见,直接使用时间戳的16进制,实际使用时可以加上客户端的ip地址,mac地址等信息做个哈希之后,作为nonce参数。
我们将每次请求的nonce参数存储到一个“集合”中,可以json格式存储到数据库或缓存中。
每次处理HTTP请求时,首先判断该请求的nonce参数是否在该“集合”中,如果存在则认为是非法请求。
nonce参数在首次请求时,已经被存储到了服务器上的“集合”中,再次发送请求会被识别并拒绝。
nonce参数作为数字签名的一部分,是无法篡改的,因为黑客不清楚token,所以不能生成新的sign。
这种方式也有很大的问题,那就是存储nonce参数的“集合”会越来越大,验证nonce是否存在“集合”中的耗时会越来越长。我们不能让nonce“集合”无限大,所以需要定期清理该“集合”,但是一旦该“集合”被清理,我们就无法验证被清理了的nonce参数了。也就是说,假设该“集合”平均1天清理一次的话,我们抓取到的该url,虽然当时无法进行重放攻击,但是我们还是可以每隔一天进行一次重放攻击的。而且存储24小时内,所有请求的“nonce”参数,也是一笔不小的开销。
基于timestamp和nonce的方案
nonce的一次性可以解决timestamp参数60s的问题,timestamp可以解决nonce参数“集合”越来越大的问题。
防止重放攻击一般和防止请求参数被串改一起做,请求的Headers数据如下图所示。
我们在timestamp方案的基础上,加上nonce参数,因为timstamp参数对于超过60s的请求,都认为非法请求,所以我们只需要存储60s的nonce参数的“集合”即可。
HTTP请求头,参数说明:
由于每次数据请求都要带上这几个参数,所以直接将这几个参数设置在请求头中,从而简化body长度;当然也可以在去请求参数中拼凑视具体情况而定。
参数名 | 参数说明 | 备注 |
---|---|---|
token | 用户令牌,用于认证用户身份 | 稍微长一点的文本 |
sign | 签名,用于监测请求数据的完整性 | 中等文本 |
timestamp | 请求时间戳 | 根据的约定的保留毫秒或者精度到秒 |
nonce | 请求随机字符串: MD5(时间戳+随机字符) | 单位时间内产生不重复字符就好 |
有关token的详细说明,请查看《Access Token & Refresh Token 机制》这篇文章
服务器端校验:
服务器端的校验配置通常会放在Filter / Interceptor 中,作为全局的管理。
nonce参数通常会存在redis中,并且设置TTL过期时间。
1 | String token = request.getHeader("token"); |
非对称密钥
拓扑结构
与对称密钥不同,非对称密钥要有一组密钥分别是公钥和私钥,通常情况下公钥加密,私钥解密。公钥可以发布给任意的客户端程序,服务器端则通过私钥解密。以下是非对称密钥的拓扑结构。
如上图所以,发送者用接收方公开出来的公钥PK进行加密。接受方在收到密文后,再用与公钥对应的私钥SK进行解密。同样,密文即便被截获,但是由于截获者只有公钥,没有私钥,他不能进行解密
非对称加密优缺点
非对称加密的突出优点是用于解密的密钥(也就是私钥)永远不需要传递给对方。但是,它的缺点也很突出:非对称加密算法复杂,导致加解密速度慢,故只适合小量数据的场合。而对称加密加解密效率高,系统开销小,适合进行大数据量的加解密。由于文件一般比较大,这个特性决定了适合它的加密方式最好是对称加密。
Eg.RSA对称密钥算法(java实现)
1 |
|
1 | package com.fanfq.util.commons.encrypt.rsa; |