Skip to content

安全规范 ​

HTTP Body 是加密包装,不是业务 JSON。必须先解密。业务字段全部在解密后的明文内。

推送、Token、查询、回调均使用同一密文外壳(取 Token 时额外带明文 enterpriseId)。

密文外壳 ​

http
POST /hook HTTP/1.1
Host: your.example
Content-Type: application/json
json
{
  "specVersion": "1",
  "alg": "A256GCM",
  "keyVersion": 1,
  "eventId": "c0ffee12ab",
  "nonce": "<Base64,12字节 IV>",
  "ciphertext": "<Base64,密文,不含 tag>",
  "tag": "<Base64,16字节 tag>"
}
参数名类型必传描述
specVersionstring是固定 "1"
algstring是固定 A256GCM
keyVersionint是密钥版本,轮换后 +1
eventIdstring是本条推送 / 请求 ID;推送侧与明文中相同,用作去重
noncestring是GCM IV。每次推送(含补推)都更换,不可作去重键
ciphertextstring是密文 Base64,不含 tag
tagstring是GCM 认证 tag,16 字节

解密步骤 ​

  1. Base64 解码 secret、nonce、ciphertext、tag。
  2. 拼接字节:ciphertext ‖ tag(先密文,后 16 字节 tag)。
  3. AES-256-GCM:AES/GCM/NoPadding,IV = nonce(12 字节),tag 128 bit,无 AAD。
  4. 明文为 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