解密示例代码
以下示例演示如何接收回调请求并解密得到原始 EnvelopeCallbackPayload JSON。CallbackKey 为配置回调时由控制台生成的 32 字符密钥。
Go
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
// CallbackRequest 回调请求体
type CallbackRequest struct {
Encrypt string `json:"encrypt"`
}
// EnvelopeCallbackPayload 回调 Payload
type EnvelopeCallbackPayload struct {
EventType string `json:"EventType"`
EnvelopeId string `json:"EnvelopeId"`
OccurredAt string `json:"OccurredAt"`
PreviousEnvelopeStatus string `json:"PreviousEnvelopeStatus"`
CurrentEnvelopeStatus string `json:"CurrentEnvelopeStatus"`
VoidReason string `json:"VoidReason,omitempty"`
Recipients []RecipientCallbackInfo `json:"Recipients"`
}
// RecipientCallbackInfo 接收人回调信息
type RecipientCallbackInfo struct {
RecipientId string `json:"RecipientId"`
Name string `json:"Name"`
Email string `json:"Email"`
SigningOrder int `json:"SigningOrder"`
PreviousStatus string `json:"PreviousStatus"`
CurrentStatus string `json:"CurrentStatus"`
OperatedAt string `json:"OperatedAt"`
DeclineReason string `json:"DeclineReason,omitempty"`
}
// decryptPayload 解密回调数据
// encryptedBase64: 回调请求中 encrypt 字段的值
// callbackKey: 控制台生成的 32 字符 CallbackKey
func decryptPayload(encryptedBase64 string, callbackKey string) ([]byte, error) {
// 1. Base64 解码
cipherData, err := base64.StdEncoding.DecodeString(encryptedBase64)
if err != nil {
return nil, fmt.Errorf("base64 decode failed: %w", err)
}
// 2. 构造 AES-256-GCM 解密器
keyBytes := []byte(callbackKey)
if len(keyBytes) != 32 {
return nil, fmt.Errorf("callback key must be 32 bytes, got %d", len(keyBytes))
}
block, err := aes.NewCipher(keyBytes)
if err != nil {
return nil, fmt.Errorf("create AES cipher failed: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("create GCM failed: %w", err)
}
// 3. 分离 nonce(前 12 字节)和密文
nonceSize := gcm.NonceSize() // 12
if len(cipherData) < nonceSize {
return nil, fmt.Errorf("ciphertext too short")
}
nonce := cipherData[:nonceSize]
ciphertext := cipherData[nonceSize:]
// 4. GCM 解密
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("GCM decrypt failed: %w", err)
}
return plaintext, nil
}
func callbackHandler(w http.ResponseWriter, r *http.Request) {
const callbackKey = "your-32-char-callback-key-here!!" // 替换为控制台生成的 CallbackKey
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read body failed", http.StatusBadRequest)
return
}
var req CallbackRequest
if err := json.Unmarshal(body, &req); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
// 解密
plaintext, err := decryptPayload(req.Encrypt, callbackKey)
if err != nil {
log.Printf("decrypt failed: %v", err)
http.Error(w, "decrypt failed", http.StatusBadRequest)
return
}
// 解析 Payload
var payload EnvelopeCallbackPayload
if err := json.Unmarshal(plaintext, &payload); err != nil {
log.Printf("unmarshal payload failed: %v", err)
http.Error(w, "invalid payload", http.StatusBadRequest)
return
}
// 处理回调事件
log.Printf("Received event: %s, EnvelopeId: %s", payload.EventType, payload.EnvelopeId)
// 返回 200 表示处理成功
w.WriteHeader(http.StatusOK)
}
func main() {
http.HandleFunc("/callback", callbackHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}Python
依赖:pip install pycryptodome # 或 pip install pycryptodomex
import base64
import json
from http.server import HTTPServer, BaseHTTPRequestHandler
from Crypto.Cipher import AES
CALLBACK_KEY = "your-32-char-callback-key-here!!" # 替换为控制台生成的 CallbackKey
def decrypt_payload(encrypted_base64: str, callback_key: str) -> dict:
"""解密回调数据
Args:
encrypted_base64: 回调请求中 encrypt 字段的值
callback_key: 控制台生成的 32 字符 CallbackKey
Returns:
解密后的 Payload 字典
"""
# 1. Base64 解码
cipher_data = base64.b64decode(encrypted_base64)
# 2. 分离 nonce(前 12 字节)和密文+tag
nonce = cipher_data[:12]
ciphertext_with_tag = cipher_data[12:]
# 3. 分离密文和 GCM auth tag(最后 16 字节)
ciphertext = ciphertext_with_tag[:-16]
tag = ciphertext_with_tag[-16:]
# 4. AES-256-GCM 解密
key_bytes = callback_key.encode('utf-8')
cipher = AES.new(key_bytes, AES.MODE_GCM, nonce=nonce)
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
# 5. 解析 JSON
return json.loads(plaintext.decode('utf-8'))
class CallbackHandler(BaseHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length)
try:
req = json.loads(body)
payload = decrypt_payload(req['encrypt'], CALLBACK_KEY)
# 处理回调事件
print(f"Received event: {payload['EventType']}, "
f"EnvelopeId: {payload['EnvelopeId']}")
# 返回 200 表示处理成功
self.send_response(200)
self.end_headers()
except Exception as e:
print(f"Error processing callback: {e}")
self.send_response(400)
self.end_headers()
if __name__ == '__main__':
server = HTTPServer(('0.0.0.0', 8080), CallbackHandler)
print('Callback server listening on port 8080...')
server.serve_forever()Java
依赖:Java 8+ (javax.crypto 内置)
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
/**
* Tencent eSign 回调解密工具类
*
* 密文格式:Base64( nonce[12字节] + GCM_ciphertext + GCM_tag[16字节] )
*/
public class CallbackDecryptor {
private static final int NONCE_LENGTH = 12;
private static final int TAG_LENGTH_BITS = 128; // 16 bytes
/**
* 解密回调数据
*
* @param encryptedBase64 回调请求中 encrypt 字段的值
* @param callbackKey 控制台生成的 32 字符 CallbackKey
* @return 解密后的 JSON 字符串
*/
public static String decryptPayload(String encryptedBase64, String callbackKey) throws Exception {
// 1. Base64 解码
byte[] cipherData = Base64.getDecoder().decode(encryptedBase64);
// 2. 分离 nonce(前 12 字节)和密文
byte[] nonce = Arrays.copyOfRange(cipherData, 0, NONCE_LENGTH);
byte[] ciphertext = Arrays.copyOfRange(cipherData, NONCE_LENGTH, cipherData.length);
// 3. 构造 AES-256-GCM 解密器
byte[] keyBytes = callbackKey.getBytes(StandardCharsets.UTF_8);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
GCMParameterSpec gcmSpec = new GCMParameterSpec(TAG_LENGTH_BITS, nonce);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);
// 4. 解密
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, StandardCharsets.UTF_8);
}
}
// ====== Spring Boot Controller 示例 ======
//
// import com.fasterxml.jackson.databind.JsonNode;
// import com.fasterxml.jackson.databind.ObjectMapper;
// import org.springframework.web.bind.annotation.*;
//
// @RestController
// public class CallbackController {
//
// private static final String CALLBACK_KEY = "your-32-char-callback-key-here!!"; // 替换为控制台生成的 CallbackKey
// private final ObjectMapper objectMapper = new ObjectMapper();
//
// @PostMapping("/callback")
// public void handleCallback(@RequestBody JsonNode body) throws Exception {
// String encryptedData = body.get("encrypt").asText();
//
// // 解密
// String payloadJson = CallbackDecryptor.decryptPayload(encryptedData, CALLBACK_KEY);
//
// // 解析 Payload
// JsonNode payload = objectMapper.readTree(payloadJson);
// String eventType = payload.get("EventType").asText();
// String envelopeId = payload.get("EnvelopeId").asText();
//
// System.out.printf("Received event: %s, EnvelopeId: %s%n", eventType, envelopeId);
// }
// }Node.js
依赖:Node.js 12+(crypto 模块内置)
const crypto = require('crypto');
const http = require('http');
const CALLBACK_KEY = 'your-32-char-callback-key-here!!'; // 替换为控制台生成的 CallbackKey
/**
* 解密回调数据
* @param {string} encryptedBase64 - 回调请求中 encrypt 字段的值
* @param {string} callbackKey - 控制台生成的 32 字符 CallbackKey
* @returns {object} 解密后的 Payload 对象
*/
function decryptPayload(encryptedBase64, callbackKey) {
// 1. Base64 解码
const cipherData = Buffer.from(encryptedBase64, 'base64');
// 2. 分离 nonce(前 12 字节)、密文和 auth tag(最后 16 字节)
const nonce = cipherData.subarray(0, 12);
const authTag = cipherData.subarray(cipherData.length - 16);
const ciphertext = cipherData.subarray(12, cipherData.length - 16);
// 3. AES-256-GCM 解密
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
Buffer.from(callbackKey, 'utf8'),
nonce
);
decipher.setAuthTag(authTag);
// 4. 解密得到明文
let plaintext = decipher.update(ciphertext);
plaintext = Buffer.concat([plaintext, decipher.final()]);
// 5. 解析 JSON
return JSON.parse(plaintext.toString('utf8'));
}
// HTTP 服务器接收回调
const server = http.createServer((req, res) => {
if (req.method !== 'POST') {
res.writeHead(405);
res.end();
return;
}
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
try {
const { encrypt } = JSON.parse(body);
const payload = decryptPayload(encrypt, CALLBACK_KEY);
// 处理回调事件
console.log(`Received event: ${payload.EventType}, EnvelopeId: ${payload.EnvelopeId}`);
// 返回 200 表示处理成功
res.writeHead(200);
res.end();
} catch (err) {
console.error('Error processing callback:', err);
res.writeHead(400);
res.end();
}
});
});
server.listen(8080, () => {
console.log('Callback server listening on port 8080...');
});C# (.NET)
依赖:.NET 6+(System.Security.Cryptography 内置)
using System;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
/// <summary>
/// Tencent eSign 回调解密工具类
/// </summary>
public static class CallbackDecryptor
{
/// <summary>
/// 解密回调数据
/// </summary>
/// <param name="encryptedBase64">回调请求中 encrypt 字段的值</param>
/// <param name="callbackKey">控制台生成的 32 字符 CallbackKey</param>
/// <returns>解密后的 JSON 字符串</returns>
public static string DecryptPayload(string encryptedBase64, string callbackKey)
{
// 1. Base64 解码
byte[] cipherData = Convert.FromBase64String(encryptedBase64);
// 2. 分离 nonce(前 12 字节)、密文和 auth tag(最后 16 字节)
byte[] nonce = cipherData[..12];
byte[] tag = cipherData[^16..];
byte[] ciphertext = cipherData[12..^16];
// 3. AES-256-GCM 解密
byte[] keyBytes = Encoding.UTF8.GetBytes(callbackKey);
byte[] plaintext = new byte[ciphertext.Length];
using var aes = new AesGcm(keyBytes, 16);
aes.Decrypt(nonce, ciphertext, tag, plaintext);
return Encoding.UTF8.GetString(plaintext);
}
}
// ====== ASP.NET Core Minimal API 示例 ======
//
// var builder = WebApplication.CreateBuilder(args);
// var app = builder.Build();
// const string callbackKey = "your-32-char-callback-key-here!!"; // 替换为控制台生成的 CallbackKey
//
// app.MapPost("/callback", async (HttpRequest request) =>
// {
// var body = await JsonSerializer.DeserializeAsync<JsonElement>(request.Body);
// var encryptedData = body.GetProperty("encrypt").GetString()!;
//
// var payloadJson = CallbackDecryptor.DecryptPayload(encryptedData, callbackKey);
// var payload = JsonSerializer.Deserialize<JsonElement>(payloadJson);
//
// Console.WriteLine($"Received event: {payload.GetProperty("EventType")}, "
// + $"EnvelopeId: {payload.GetProperty("EnvelopeId")}");
//
// return Results.Ok();
// });
//
// app.Run();PHP
依赖:PHP 7.1+(openssl 扩展内置)
<?php
/**
* 解密 Tencent eSign 回调数据
*
* @param string $encryptedBase64 回调请求中 encrypt 字段的值
* @param string $callbackKey 控制台生成的 32 字符 CallbackKey
* @return array 解密后的 Payload 数组
*/
function decryptPayload(string $encryptedBase64, string $callbackKey): array
{
// 1. Base64 解码
$cipherData = base64_decode($encryptedBase64, true);
if ($cipherData === false) {
throw new RuntimeException('Base64 decode failed');
}
// 2. 分离 nonce(前 12 字节)、密文和 auth tag(最后 16 字节)
$nonce = substr($cipherData, 0, 12);
$tag = substr($cipherData, -16);
$ciphertext = substr($cipherData, 12, -16);
// 3. AES-256-GCM 解密
$plaintext = openssl_decrypt(
$ciphertext,
'aes-256-gcm',
$callbackKey,
OPENSSL_RAW_DATA,
$nonce,
$tag
);
if ($plaintext === false) {
throw new RuntimeException('AES-GCM decrypt failed');
}
// 4. 解析 JSON
return json_decode($plaintext, true, 512, JSON_THROW_ON_ERROR);
}
// ====== 接收回调示例 ======
$callbackKey = 'your-32-char-callback-key-here!!'; // 替换为控制台生成的 CallbackKey
$body = file_get_contents('php://input');
$req = json_decode($body, true);
try {
$payload = decryptPayload($req['encrypt'], $callbackKey);
// 处理回调事件
error_log(sprintf(
'Received event: %s, EnvelopeId: %s',
$payload['EventType'],
$payload['EnvelopeId']
));
// 返回 200 表示处理成功
http_response_code(200);
} catch (Throwable $e) {
error_log('Error processing callback: ' . $e->getMessage());
http_response_code(400);
}