DingTalkCrypto.php 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. <?php
  2. namespace App\Service;
  3. class DingTalkCrypto
  4. {
  5. protected $token;
  6. protected $encodingAesKey; // URL safe base64 字符串,长度 43
  7. // protected $corpid; // 如果需要
  8. public function __construct(string $token, string $encodingAesKey)
  9. {
  10. $this->token = $token;
  11. $this->encodingAesKey = $encodingAesKey;
  12. }
  13. /**
  14. * 验签 + 解密消息体
  15. *
  16. * @param string $signature 从请求 query 参数 signature
  17. * @param string $timestamp 从请求参数 timestamp
  18. * @param string $nonce 从请求参数 nonce
  19. * @param string $encrypt 消息体中的 encrypt 字段
  20. * @return array|false 解密后的数组,失败返回 false
  21. */
  22. public function decryptMsg(string $signature, string $timestamp, string $nonce, string $encrypt)
  23. {
  24. // 1. 验签
  25. $tmpArr = [$this->token, $timestamp, $nonce, $encrypt];
  26. sort($tmpArr, SORT_STRING);
  27. $tmpStr = implode($tmpArr);
  28. $hash = sha1($tmpStr);
  29. if ($hash !== $signature) {
  30. return false;
  31. }
  32. // 2. 解密
  33. $aesKey = base64_decode($this->encodingAesKey . '='); // 注意要加一个 '=' 补足 padding
  34. $iv = substr($aesKey, 0, 16);
  35. // 解密,使用 AES-256-CBC
  36. $cipherText = base64_decode($encrypt);
  37. $decrypted = openssl_decrypt($cipherText, 'AES-256-CBC', $aesKey, OPENSSL_ZERO_PADDING, $iv);
  38. if ($decrypted === false) {
  39. return false;
  40. }
  41. // 去掉 padding, PKCS#7
  42. $decrypted = $this->pkcs7Unpad($decrypted);
  43. // 解出: 16 位随机字符串 + 4 字节内容长度 network-order + 内容 + corpId(如果有的话)
  44. // 跳过前 16 位
  45. $content = substr($decrypted, 16);
  46. // 读长度
  47. $lenList = unpack("N", substr($content, 0, 4));
  48. $xmlLen = $lenList[1];
  49. $xmlContent = substr($content, 4, $xmlLen);
  50. // 如果有 corpId 在 xmlContent 后还有
  51. // $fromCorpId = substr($content, 4 + $xmlLen);
  52. $json = $xmlContent; // 钉钉回调 body 是 JSON 字符串
  53. // 解析
  54. $arr = json_decode($json, true);
  55. return $arr;
  56. }
  57. /**
  58. * 加密回复消息,比如返回 “success”
  59. *
  60. * @param string $replyMsg 明文消息,比如 "success"
  61. * @param string $timestamp
  62. * @param string $nonce
  63. * @return array 包含加密后json结构
  64. */
  65. public function encryptMsg(string $replyMsg, string $timestamp, string $nonce)
  66. {
  67. // 1. 构造消息内容
  68. $random16 = $this->getRandomStr(16);
  69. $replyMsgXml = $replyMsg; // 钉钉回调用 JSON 格式内容
  70. $msgLen = strlen($replyMsgXml);
  71. $msgLenBin = pack("N", $msgLen);
  72. $corpId = ''; // 如果不需要 corpId,可以空
  73. $plain = $random16 . $msgLenBin . $replyMsgXml . $corpId;
  74. // 2. PKCS#7 padding
  75. $blockSize = 32;
  76. $pad = $blockSize - (strlen($plain) % $blockSize);
  77. if ($pad == 0) {
  78. $pad = $blockSize;
  79. }
  80. $plainPadded = $plain . str_repeat(chr($pad), $pad);
  81. // 3. AES 加密
  82. $aesKey = base64_decode($this->encodingAesKey . '=');
  83. $iv = substr($aesKey, 0, 16);
  84. $encrypted = openssl_encrypt($plainPadded, 'AES-256-CBC', $aesKey, OPENSSL_ZERO_PADDING, $iv);
  85. $encryptBase64 = base64_encode($encrypted);
  86. // 4. 签名
  87. $signatureArr = [$this->token, $timestamp, $nonce, $encryptBase64];
  88. sort($signatureArr, SORT_STRING);
  89. $signature = sha1(implode($signatureArr));
  90. // 5. 返回结构
  91. return [
  92. 'msg_signature' => $signature,
  93. 'encrypt' => $encryptBase64
  94. ];
  95. }
  96. private function getRandomStr($length = 16)
  97. {
  98. $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  99. $str = '';
  100. for ($i = 0; $i < $length; $i++) {
  101. $str .= $chars[ord(bin2hex(random_bytes(1))) % strlen($chars)];
  102. }
  103. return $str;
  104. }
  105. private function pkcs7Unpad($text)
  106. {
  107. // 去掉结尾的 padding
  108. $pad = ord(substr($text, -1));
  109. if ($pad < 1 || $pad > 32) {
  110. // padding 有问题
  111. return false;
  112. }
  113. return substr($text, 0, -$pad);
  114. }
  115. }