解密示例代码

以下示例演示如何接收回调请求并解密得到原始 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);
}