智能小程序OPENCARD
接入指南

API 加密协议

为什么使用加密协议?

为维护双方数据和信息安全,保护资源方(开发者)提供的资源数据不被第三方窃取,以及双方的通讯不被中间人劫持,我们在设计与资源方 Webhook 通信接口时,使用增强加密的协议保证接口调用的安全性。

加密协议选择

为降低资源方理解和实现的复杂度,我们采取 IETF 标准提案 RFC5861 JWE (JSON Web Encryption) 对应用层 JSON 数据进行数据加密、完整性保护和来源认证,加密后的内容通过 HTTP 协议传输。协议数据结构如下图所示:

API 加密协议

作为开放的互联网标准提案,JWE 有很多种开源实现。资源方可以根据自身需求,选择合适的开源函数库集成到后台服务中。

协议细节和示例

发送给 webhook 的请求

小程序开放阿拉丁发送给 webhook 的请求 JSON Object,主要由以下公共字段组成:

1
2
3
4
5
6
7
{
"type": "sp_ala", // 智能小程序阿拉丁请求,以便与其它请求区分
"srcid": "123", // 卡片的 ID,每个卡片不同
"surface": "mobile", // 卡片展示在哪个产品上, mobile: 支持小程序的移动搜索,web_h5: 支持 H5 的移动搜索
"intent": { // 卡片的 intent,每个卡片不同
}
}

Webhook 返回的结果

Webhook 返回的结果 JSON Object,主要由以下公共字段组成:

1
2
3
4
5
6
7
{
"status": 0, // 结果状态码,0 代表正确,1 代表无结果,2 代表请求参数错误,3 代表内部服务错误
"msg": "", // 出错消息
"data": { // 资源的结果内容,每个资源分类不同
},
"lifetime": 1559705004 // 可选字段,秒级时间戳,指示该数据的有效期
}

对请求和结果 JSON 消息加密

为了降低加密复杂度和计算开销,JWE 选择的加密算法组合是:

1
2
3
4
{
"alg":"A128KW",
"enc":"A128CBC-HS256"
}

这一组合。其中:

  • A128KW 代表密钥生成算法通过预共享密钥 PSK (Pre Shared Key) 生成每次会话使用的内容加密密钥 CEK (Content Encryption Key)

  • A128CBC-HS256 代表加密算法底层使用 AES_128_CBC_HMAC_SHA_256 算法产生密文以及认证 TAG。除了保护了数据内容之外,同时对数据的来源和完整性进行了签名。

同时,为了尽量节省带宽,我们采取 Compact 格式序列化 JWE 对象

加密参数

JWE 需要传入预共享密钥 PSK 参数进行初始化。在我们的加密算法组合下,PSK 是 16 字节(128位)长度的密钥,由资源方在开放平台中设置。

由于 PSK 允许为任意二进制字节串,包括不可打印字符。为便于输入和展示,资源方需要在平台中输入 base64url(PSK),即经过 base64url 编码的 PSKPSK仅存储在双方服务器中,用于加密内容密钥 CEK,不会在消息中传递。

在 JWE 的 protected header 里,需要额外传输一个 kid 字段,向对方标识使用的是哪个 PSKkid 主要为了便于在开发者更新 PSK 时,双方加密通信不受影响。

开发者每一次更新 PSKkid 都会 +1。开发者在开放平台页面上,可以看到当前 PSK 对应的 kid 值。当开发者更新 PSK 时,已经收到更新的开放平台服务器会用新的 kid,PSK 组合请求 webhook,未收到更新的开放平台服务器会使用旧的 kid,PSK 组合请求 webhook。资源方可以通过 kid 判断应该使用哪个 PSK 解密请求消息和加密返回消息。

注意

  1. 在实现加解密的过程中,开发者服务器需要维护一张映射表,指定 kidPSK 之间的映射关系,保留历次更新版本,开发者每一次更新 PSKkid 都会更新。
  2. 通过 开放平台 -> 个人中心 -> 设置中心 -> 小程序开放安全配置,查看当前 kidPSK 的之间的映射,kid 会随请求下发,接收方服务器需要使用同一个 kid,PSK 组合加密返回消息,以免发送方服务器无法解密返回消息内容。

会话参数

为了便于双方进行调试或者日志追查,请求方需要在 JWE 的 protected header 里额外传输一个 rid 字段,代表请求会话 ID。rid 的格式为 毫秒时间戳-随机数,响应方需将 rid 原样返回给发送方以保证响应的是最新的请求。

rid放在 JWE 协议的 protected header 中,没有被加密,可以通过 base64url decode 直接解码出来。但 rid 是签名内容的一部分,也就是说 rid 如果被篡改会导致整个消息签名不通过。

示例

我们下面以一个例子说明一个遵从 JWE 标准的序列化消息生成过程。开发者可以使用同样的参数对序列化后的 JWE 消息进行解密,以验证加解密实现是否正确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# INPUT:
# PSKBase64url: "MDEyMzQ1Njc4OWFiY2RlZg" 即 base64url("0123456789abcdef")
# kid: "0"
# rid: "1559123682789-315431431"
# text: {
# "type":"sp_ala",
# "srcid":"123",
# "surface":"mobile",
# "intent":{
# "query":"hello"
# }
# }

# Python Example Code
from jwcrypto import jwk, jwe
from jwcrypto.common import json_encode

# 使用 JWK 标准格式导入对称密钥
key = jwk.JWK.from_json('{"kty":"oct","k":"MDEyMzQ1Njc4OWFiY2RlZg"}')
# 要传输的内容明文,即应用层数据
text = json_encode({"type":"sp_ala","srcid":"123","surface":"mobile","intent":{"query":"hello"}})
# 根据输入参数生成 JWE 对象
jwetoken = jwe.JWE(plaintext=text.encode('utf-8'),
protected=json_encode({"alg": "A128KW", "enc":"A128CBC-HS256","kid":"0","rid":"1559123682789-315431431"}),
recipient=key)
# JWE JSON 序列化:
print jwetoken.serialize()
# 输出结果:由于 CEK 和 IV 在每轮会话中都会重新生成,所以输出加密内容每次运行时都不同
# JSON 格式含省略内容,仅供理解,实际上使用的协议是下文 Compact 序列化格式
#{
# "protected":"eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q......",
# "encrypted_key":"KUiCWj2y24pCjQ6urYkYLZJJfF172Rvjo_wW8lSvKv5HogoKBGfx3g",
# "iv":"uN51R6JUGwMoVDH1cKFBRA",
# "ciphertext":"wbuvYqJEG2iVCEG2mzozAM3e6ymstfxH......",
# "tag":"2RokPmpNfg20YcW2czuYnQ"
#}
# JWE Compact 序列化(最终发送的消息格式)
print jwetoken.serialize(compact=True)
# eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoiMCIsInJpZCI6IjE1NTkxMjM2ODI3ODktMzE1NDMxNDMxIn0.KUiCWj2y24pCjQ6urYkYLZJJfF172Rvjo_wW8lSvKv5HogoKBGfx3g.uN51R6JUGwMoVDH1cKFBRA.wbuvYqJEG2iVCEG2mzozAM3e6ymstfxH-f5yanNyBmMhQt1F_Jwd4fgJlf0A9hu9GkgIrz5cwayGlzvObFbwbNAOGIfzNPfF8gjOcqD1ahc.2RokPmpNfg20YcW2czuYnQ

通过 HTTP POST 发送

传输层我们采取规范的 HTTP 传输协议,将加密层的内容放入 HTTP body 中,使用 POST 请求发送给接收方。待发送的字符串会以原串的方式放到 body 中,设置 HTTP Header “Content-Type: application/jwt“ (遵从 RFC7519 Section 10.3 约定)表明数据格式是 JWE 加密数据,以示与常见的 x-www-form-urlencoded 数据区分。

应用层的错误会有应用层的错误码处理,但由于我们仅支持加密传输,未解密前无法看到消息内容。如果发送方采取了不恰当的加密方式(例如出现了bug),接收方无法解密应用层内容,需要返回对应的错误码。我们复用 HTTP 错误码 400 加纯文本出错内容来实现这一功能。

1
2
HTTP status: 400
HTTP body: Cannot decode JWE content.
反 馈帮 助 回 到顶 部