.NET中Telegram Web推送加密消息解密实现问询
解密Telegram AES-GCM加密推送消息
核心流程概述
Telegram推送遵循Web Push标准的AES-GCM加密机制,解密需完成解析请求参数、计算ECDH共享密钥、派生加密密钥/IV、AES-GCM解密四个关键环节,以下是具体实现细节:
1. 解析请求头中的加密参数
从收到的HTTP请求头中提取三个核心值:
- Salt:从
Encryption头(格式为salt=xxx)中取出值,用WebEncoders.Base64UrlDecode解码为字节数组 - Telegram公钥:从
Crypto-Key头(格式为dh=xxx)中取出值,用WebEncoders.Base64UrlDecode解码为字节数组 - 本地Auth密钥:即你注册时生成的
"rnd96257410358jh"对应的原始字节数组(无需再解码)
2. 计算ECDH共享密钥
利用你已生成的本地EC私钥,结合Telegram的公钥计算共享密钥:
// 初始化P-256曲线参数 var ecParameters = Org.BouncyCastle.Asn1.Nist.NistNamedCurves.GetByName("P-256"); var ecSpec = new ECDomainParameters(ecParameters.Curve, ecParameters.G, ecParameters.N, ecParameters.H, ecParameters.GetSeed()); // 解析Telegram的公钥 var telegramPubKeyPoint = ecParameters.Curve.DecodePoint(telegramDhBytes); var telegramPubKey = new ECPublicKeyParameters(telegramPubKeyPoint, ecSpec); // 加载本地私钥(替换为你持久化存储的私钥字节数组) var localPrivateKey = new ECPrivateKeyParameters(new BigInteger(1, _localPrivateKey), ecSpec); // 计算ECDH共享密钥 var agreement = AgreementUtilities.GetBasicAgreement("ECDH"); agreement.Init(localPrivateKey); var sharedSecret = agreement.CalculateAgreement(telegramPubKey).ToByteArrayUnsigned(); // 修正共享密钥长度为32字节(P-256曲线标准长度) if (sharedSecret.Length != 32) { var temp = new byte[32]; Array.Copy(sharedSecret, 0, temp, 32 - sharedSecret.Length, sharedSecret.Length); sharedSecret = temp; }
3. 用HKDF派生AES密钥和IV
Web Push标准使用HKDF算法派生加密所需的AES密钥和IV:
// 初始化HKDF生成器(基于HMAC-SHA256) var hkdf = new HkdfBytesGenerator(new Sha256Digest()); hkdf.Init(new HkdfParameters( sharedSecret, saltBytes, Encoding.UTF8.GetBytes("Content-Encoding: aesgcm\0") // 固定信息字段 )); // 派生28字节数据:16字节AES密钥 + 12字节IV var derivedBytes = new byte[28]; hkdf.GenerateBytes(derivedBytes, 0, derivedBytes.Length); var aesKey = derivedBytes.Take(16).ToArray(); var iv = derivedBytes.Skip(16).Take(12).ToArray();
4. AES-GCM解密消息体
Telegram推送的消息体结构为16字节认证标签 + 密文内容,拆分后使用.NET原生AesGcm解密:
// 读取请求体字节数组 var encryptedData = await Request.BodyReader.ReadAsync().AsTask(); var encryptedBytes = encryptedData.Buffer.ToArray(); // 拆分标签和密文 var tag = encryptedBytes.Take(16).ToArray(); var ciphertext = encryptedBytes.Skip(16).ToArray(); // 执行解密 var plaintext = new byte[ciphertext.Length]; using var aesGcm = new AesGcm(aesKey); aesGcm.Decrypt(iv, ciphertext, tag, plaintext); // 解密后为JSON格式,可直接转换为字符串处理 var pushContent = Encoding.UTF8.GetString(plaintext);
完整ASP.NET Core端点示例
将以上逻辑整合到控制器中:
[ApiController] [Route("telegrampush")] public class TelegramPushController : ControllerBase { // 从持久化存储加载本地私钥和Auth密钥(示例中需替换为实际存储逻辑) private readonly byte[] _localPrivateKey = /* 你的本地私钥字节数组 */; private readonly byte[] _authKey = Encoding.UTF8.GetBytes("rnd96257410358jh"); [HttpPost] public async Task<IActionResult> ReceivePush() { // 解析请求头 var encryptionHeader = Request.Headers["Encryption"].FirstOrDefault(); var cryptoKeyHeader = Request.Headers["Crypto-Key"].FirstOrDefault(); if (string.IsNullOrEmpty(encryptionHeader) || string.IsNullOrEmpty(cryptoKeyHeader)) return BadRequest("缺失加密相关请求头"); var saltBytes = WebEncoders.Base64UrlDecode(encryptionHeader.Split('=')[1]); var telegramDhBytes = WebEncoders.Base64UrlDecode(cryptoKeyHeader.Split('=')[1]); // 计算ECDH共享密钥 var ecParameters = Org.BouncyCastle.Asn1.Nist.NistNamedCurves.GetByName("P-256"); var ecSpec = new ECDomainParameters(ecParameters.Curve, ecParameters.G, ecParameters.N, ecParameters.H, ecParameters.GetSeed()); var telegramPubKeyPoint = ecParameters.Curve.DecodePoint(telegramDhBytes); var telegramPubKey = new ECPublicKeyParameters(telegramPubKeyPoint, ecSpec); var localPrivateKey = new ECPrivateKeyParameters(new BigInteger(1, _localPrivateKey), ecSpec); var agreement = AgreementUtilities.GetBasicAgreement("ECDH"); agreement.Init(localPrivateKey); var sharedSecret = agreement.CalculateAgreement(telegramPubKey).ToByteArrayUnsigned(); if (sharedSecret.Length != 32) { var temp = new byte[32]; Array.Copy(sharedSecret, 0, temp, 32 - sharedSecret.Length, sharedSecret.Length); sharedSecret = temp; } // 派生AES密钥和IV var hkdf = new HkdfBytesGenerator(new Sha256Digest()); hkdf.Init(new HkdfParameters(sharedSecret, saltBytes, Encoding.UTF8.GetBytes("Content-Encoding: aesgcm\0"))); var derivedBytes = new byte[28]; hkdf.GenerateBytes(derivedBytes, 0, derivedBytes.Length); var aesKey = derivedBytes.Take(16).ToArray(); var iv = derivedBytes.Skip(16).Take(12).ToArray(); // 解密消息体 var encryptedData = await Request.BodyReader.ReadAsync().AsTask(); var encryptedBytes = encryptedData.Buffer.ToArray(); var tag = encryptedBytes.Take(16).ToArray(); var ciphertext = encryptedBytes.Skip(16).ToArray(); var plaintext = new byte[ciphertext.Length]; using var aesGcm = new AesGcm(aesKey); aesGcm.Decrypt(iv, ciphertext, tag, plaintext); // 处理解密后的业务逻辑(示例:输出日志) var messageContent = Encoding.UTF8.GetString(plaintext); Console.WriteLine("解密后的推送内容:" + messageContent); return Ok(); } }
注意事项
- 本地私钥需安全持久化存储,禁止硬编码到代码中
- 需要安装
BouncyCastle.CryptographyNuGet包以支持ECDH和HKDF操作 - 解密后的内容为JSON格式,包含
payload字段,对应实际的Telegram通知内容
内容的提问来源于stack exchange,提问作者Hamid Z
相关产品推荐
相关产品推荐

