123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- // tag::main[]
- = 安全通信——密钥协商
- 为确保传输数据安全,推荐使用安全协议,如 `https`,无法使用时才使用该方案(通过协商密钥算法协商会话密钥,然后通过密钥加密传输数据)。
- == 术语定义:
- * 公钥、私钥
- ** 公钥会进入网络传输的,该部分是公开的。
- ** 私钥由部分不允许公开。
- * `DH` 密钥交换(协商)算法
- ** 全称为Diffie-Hellman ,该算法仅传输双方各自的公钥即可使得双方计算出相同的key,可以使用这个key作为密钥加密信息;即使攻击者只获取到双方公钥也无法计算出Key值。
- ** 关键点:需要保证在互换双方公钥时不被中间人进行替换,否则可能遭受中间人攻击。
- * 共享密钥/会话密钥
- ** DH算法中,双方进行密钥(公钥)交换后,通过对方公钥和己方私钥生成的Key值(双方结果相同)。
- ** 只要私钥不被破解,该值就是安全的。
- * 数据密钥
- ** 真正用于加密数据的key,每次会话随机生成,使用该密钥加密敏感数据,然后用共享密钥和共享iv( `shareIv` )进行加密,随后该值将被销毁。
- ** 发送数据时,会将 `使用该值加密过的敏感数据密文` 、 `共享iv` 、 `使共享密钥加密过的数据密钥(该值)` 传递给对方,对方会先解密出 `数据密钥明文` ,然后解密出本次传输的敏感信息。
- ** 该值仅在内存中使用时临时生成和恢复,用后即销毁,因为该值是临时的,且不在网络中传输,所以可以保证敏感数据的安全。
- * 安全会话标识
- ** 类似SessionId,用于记录对方是谁,通过该值来判断是否已经进行秘钥交换、获取共享秘钥,双方都会保存该标识。
- * AES支持算法
- ** 加密用的规则,请求方所支持的AES加密种类,如果支持多个,用分号分割。这些值取决于代码实现的种类。
- * AES算法选型
- ** 响应方从请求方的AES-supports挑选一个随机的加密方法,作为本次会话中使用的AES加密算法。
- * B2S(bytes[])
- ** 代表将bytes[] 转化为字符串,一般采用base64编码。
- == 交互流程
- * 整个加密流程的实现,分成客户端部分和服务端部分,对于大部分的服务,均需实现(既会提供给服务调用,也会调用其他服务)。
- * 传输加密前,双方需要通过协商,来确定加密敏感信息所用的秘钥。
- * 每一次的协商,都会生成一个唯一的 `X-Sid` ,用于双方后续的通信。当服务端发现 `X-Sid` 过期时,需要返回约定好的错误码,让客户端重新发起协商和请求。
- === 客户端-发起协商请求
- . 生成 `ECDH` 的公私钥对(公钥 `ClientPublicKey` 和私钥 `ClientPrivateKey`)
- . 生成一个唯一的会话ID(`X-Sid`)
- . 生成客户端支持的加/解密规则(`aesSupports`)
- . 生成请求接口用的认证 `Token`
- ** Token的签名为:`<X-Sid>B2S(<ClientPublicKey>)`
- ** 如 X-Sid='ABC',B2S(<ClientPublicKey>)='XYZ',则最终为 `ABCXYZ`
- . 将Token放入header中,将S(PublicKeyA)也即转换为HEX的公钥、X-Sid、AES-supports字段放入 `Body` 中,并调用服务端的协商接口。
- . `可选步骤`:填充 `negotiateVersion` 为 `1`,代表使用第一个版本的协商算法(ECDH),该字段只是为了后续版本可能更新,只有一套方案时可以直接采用默认值,节省网络开销。
- [cols="1,1,3,3"]
- .发起协商请求所需参数
- |===
- | 位置 | 字段名 | 字段值表达式 | 说明
- | Header | X-Sid | `UUID` | 安全会话唯一标识,使用UUID即可
- | Header | Token | `<X-Sid>B2S(<ClientPublicKey>)` | 用于判断传输未被篡改
- | Body | aesSupports | `["AES-ECB-192", "AES-CBC-256"]` | 支持的算法类型,自然越多越好
- | Body | publicKey | `B2S(<ClientPublicKey>)` | 客户端公钥的64进制编码
- | Body | negotiateVersion | `"1"` | 秘钥交换算法版本,可空,默认为 `1`
- |===
- === 服务器-服务端处理协商请求
- . 收到客户端的协商请求,进行参数校验
- ** 参数完整性校验
- ** 参数格式校验
- ** `Token` 有效性校验
- . 根据 `negotiateVersion` 版本,做一些事情,这里只有ECDH,直接进行ECDH的椭圆曲线选择过程,简单起见,默认使用 `ANSI X9.62 Prime 256v1`
- . 生成自己的公私钥对(`ServerPublicKey`、`ServerPrivateKey`)
- . 对 `publicKey` 64进制解码得到客户端公钥值 `ClientPublicKey`
- . 根据 `ServerPublicKey`、`ServerPrivateKey`、`ClientPublicKey` 生成共享密钥 `shareKey`
- . 根据 `ServerPublicKey`、`ClientPublicKey` 生成共享加密矢量 `shareIv`
- . 将 `aesSupports` 与自己支持的aes加密算法列表做交集处理,在交集中随机选择一个作为本次协商的加密规则 `aesAlgorithm`
- . 生成一个协商有效时间 `expireTime`,如:30分钟
- . 缓存协商结果
- ** 包括:`X-Sid`、`aesAlgorithm`、`shareKey`、`shareIv`、`expireTime`
- . 生成一个双向认证用的Token。
- ** Token的签名为:`X-Sid` + `B2S(ServerPublicKey)` + `AES-param` + `expireTime`
- . 返回协商结果。
- [cols="1,1,3,3"]
- .响应协商请求结果
- |===
- | 位置 | 字段名 | 字段值表达式 or 举例 | 说明
- | Header | X-Sid | `UUID` | 安全会话唯一标识
- | Header | Token | `X-Sid` + `B2S(ServerPublicKey)` + `AES-param` + `expireTime` | 用于判断传输未被篡改
- | Body | aesAlgorithm | `"AES-CBC-256"` | 本次会话使用的加密算法
- | Body | publicKey | `B2S(<ServerPublicKey>)` | 服务端公钥的64进制编码
- | Body | expireTime | `1800` | 会话过期时间,秒
- |===
- === 客户端-处理协商返回结果
- . 收到服务端的协商请求,进行 `Token` 有效性校验
- . 根据 `ServerPublicKey`、`ClientPublicKey`、`ClientPrivateKey` 生成共享密钥 `shareKey`
- . 根据 `ServerPublicKey`、`ClientPublicKey` 生成共享加密矢量 `shareIv`
- . 缓存协商结果
- ** 包括:`X-Sid`、`aesAlgorithm`、`shareKey`、`shareIv`、`expireTime`
- [NOTE]
- ====
- 至此,协商阶段结束。为了保证客户端发起时,服务端信息未失效,故设计中客户端缓存需提前过期。如 `expireTime /= 2` 或减少固定时间等。
- ====
- === 客户端-发起数据传输请求
- . 查找是否已与目标服务有进行协商且未过期
- ** 未过期则使用
- ** 否则重新进行协商
- include::[tags=encrypt]
- [cols="1,1,3,3"]
- .响应协商请求结果
- |===
- | 位置 | 字段名 | 字段值表达式 or 举例 | 说明
- | Header | X-Sid | `UUID` | 安全会话唯一标识
- | Header | X-Dk | `Random` | 数据秘钥密文64进制编码
- | Header | Token | `X-Sid` + `dataCipher` | 用于判断传输未被篡改
- | Body | aesAlgorithm | "AES-CBC-256" | 本次会话使用的加密算法
- | Body | publicKey | B2S(`ServerPublicKey`) | 服务端公钥的64进制编码
- | Body | expireTime | `1800` | 会话过期时间,秒
- |===
- [NOTE]
- ====
- 为了避免客户请求前未过期,收到服务端响应时缓存意外过期情况。可以在请求前通过快照机制处理,不推荐判断是否 `即将过期` (过期时间小于接口最大超时时间)。
- ====
- === 服务端-数据传输处理
- ==== 处理前解密
- include::[tags=decrypt]
- 业务处理
- ==== 业务处理后响应前加密
- 注:与客户端请求前加密流程相同
- include::[tags=encrypt]
- === 客户端-处理服务端响应
- include::[tags=decrypt]
- 业务处理
- // end::main[]
- // tag::encrypt[]
- . 随机生成 `数据秘钥`(`dataKey`,也成临时加密秘钥)
- . 以 `dataKey` 为密钥, `shareIv` 为加密矢量,采用 `aesAlgorithm` 对应算法加密 `敏感数据`(`data`),并对其进行64进制编码得到敏感数据密文(`dataCipher`)
- . 以 `shareKey` 为密钥,`shareIv`为加密矢量,采用 `aesAlgorithm` 对应算法加密 `数据秘钥`(`dataKey`),并对其进行64进制编码得到数据秘钥密文(`dataKeyCipher`)
- . 生成认证Token,签名为:`X-Sid` + `dataCipher`
- // end::encrypt[]
- // tag::decrypt[]
- . 校验请求合法性
- ** 校验 Header 中字段的完整性
- ** 校验Token的合法性
- . 判断 `X-Sid` 对应的协商是否过期,如果过期,直接返回错误码,提示需要重新协商。
- . 从 `X-Sid` 对应的缓存中取出协商缓存
- . 对数据秘钥密文 `dataKeyCipher` base64解码,再根据 共享秘钥 `shareKey`、加密矢量 `shareIv` 以及加密算法 `aesAlgorithm` 对其解密得到 `数据秘钥`(`dataKey`)。
- . 对敏感数据密文 `dataCipher` base64解码,再根据 数据秘钥 `dataKey`、加密矢量 `shareIv` 以及加密算法 `aesAlgorithm` 对其解密得到 `敏感数据明文`(`data`)
- // end::decrypt[]
|