Appearance
安全规范
HTTP Body 是加密包装,不是业务 JSON。必须先解密。业务字段全部在解密后的明文内。
推送、Token、查询、回调均使用同一密文外壳(取 Token 时额外带明文 enterpriseId)。
密文外壳
http
POST /hook HTTP/1.1
Host: your.example
Content-Type: application/jsonjson
{
"specVersion": "1",
"alg": "A256GCM",
"keyVersion": 1,
"eventId": "c0ffee12ab",
"nonce": "<Base64,12字节 IV>",
"ciphertext": "<Base64,密文,不含 tag>",
"tag": "<Base64,16字节 tag>"
}| 参数名 | 类型 | 必传 | 描述 |
|---|---|---|---|
| specVersion | string | 是 | 固定 "1" |
| alg | string | 是 | 固定 A256GCM |
| keyVersion | int | 是 | 密钥版本,轮换后 +1 |
| eventId | string | 是 | 本条推送 / 请求 ID;推送侧与明文中相同,用作去重 |
| nonce | string | 是 | GCM IV。每次推送(含补推)都更换,不可作去重键 |
| ciphertext | string | 是 | 密文 Base64,不含 tag |
| tag | string | 是 | GCM 认证 tag,16 字节 |
解密步骤
- Base64 解码
secret、nonce、ciphertext、tag。 - 拼接字节:
ciphertext‖tag(先密文,后 16 字节 tag)。 - AES-256-GCM:
AES/GCM/NoPadding,IV =nonce(12 字节),tag 128 bit,无 AAD。 - 明文为 UTF-8 JSON。
text
Key = secret(Base64 解码后 32 字节)
IV = nonce(Base64 解码后 12 字节)
密文 = ciphertext ‖ tag
算法 = AES/GCM/NoPadding
Tag = 128 bit
AAD = 无示例代码
Java 解密(收推送)
java
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public static String decrypt(String secretBase64, String nonceBase64,
String ciphertextBase64, String tagBase64) throws Exception {
byte[] key = Base64.getDecoder().decode(secretBase64);
byte[] nonce = Base64.getDecoder().decode(nonceBase64);
byte[] ciphertext = Base64.getDecoder().decode(ciphertextBase64);
byte[] tag = Base64.getDecoder().decode(tagBase64);
byte[] packed = new byte[ciphertext.length + tag.length];
System.arraycopy(ciphertext, 0, packed, 0, ciphertext.length);
System.arraycopy(tag, 0, packed, ciphertext.length, tag.length);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(key, "AES"),
new GCMParameterSpec(128, nonce));
return new String(cipher.doFinal(packed), StandardCharsets.UTF_8);
}Java 加密(查询 / 回调)
明文如 {"audioId":"audio-xxx"},返回 [nonce, ciphertext, tag] 的 Base64。
java
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
public static String[] encrypt(String secretBase64, String plaintext) throws Exception {
byte[] key = Base64.getDecoder().decode(secretBase64);
byte[] nonce = new byte[12];
new SecureRandom().nextBytes(nonce);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE,
new SecretKeySpec(key, "AES"),
new GCMParameterSpec(128, nonce));
byte[] packed = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
int cipherLen = packed.length - 16;
byte[] ciphertext = new byte[cipherLen];
byte[] tag = new byte[16];
System.arraycopy(packed, 0, ciphertext, 0, cipherLen);
System.arraycopy(packed, cipherLen, tag, 0, 16);
return new String[]{
Base64.getEncoder().encodeToString(nonce),
Base64.getEncoder().encodeToString(ciphertext),
Base64.getEncoder().encodeToString(tag)
};
}Python 解密
python
import base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def decrypt(secret_b64, nonce_b64, ciphertext_b64, tag_b64):
packed = base64.b64decode(ciphertext_b64) + base64.b64decode(tag_b64)
return AESGCM(base64.b64decode(secret_b64)).decrypt(
base64.b64decode(nonce_b64), packed, None).decode("utf-8")依赖:pip install cryptography