DingTalkCrypto.php 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. <?php
  2. namespace App\Service;
  3. class DingTalkCrypto
  4. {
  5. protected string $token;
  6. protected string $encodingAesKey; // URL safe base64 字符串,长度 43
  7. public function __construct(string $token, string $encodingAesKey)
  8. {
  9. $this->token = $token;
  10. $this->encodingAesKey = $encodingAesKey;
  11. }
  12. /**
  13. * 解密钉钉事件回调消息
  14. *
  15. * @param string $signature URL参数 signature
  16. * @param string $timestamp URL参数 timestamp
  17. * @param string $nonce URL参数 nonce
  18. * @param string $encrypt 消息体 encrypt
  19. * @return array|false
  20. */
  21. public function decryptMsg(string $signature, string $timestamp, string $nonce, string $encrypt)
  22. {
  23. // 1. 验签(官方:token + timestamp + nonce + encrypt)
  24. $tmpStr = $this->token . $timestamp . $nonce . $encrypt;
  25. $hash = sha1($tmpStr);
  26. if ($hash !== $signature) {
  27. return false;
  28. }
  29. // 2. 解密 AES-256-CBC
  30. $aesKey = base64_decode($this->encodingAesKey); // 官方不加 "="
  31. $iv = substr($aesKey, 0, 16);
  32. $cipherText = base64_decode($encrypt);
  33. $decrypted = openssl_decrypt($cipherText, 'AES-256-CBC', $aesKey, OPENSSL_ZERO_PADDING, $iv);
  34. if ($decrypted === false) return false;
  35. // 3. 去 padding
  36. $decrypted = $this->pkcs7Unpad($decrypted);
  37. // 4. 去掉前 16 位随机字符串
  38. $content = substr($decrypted, 16);
  39. // 5. 读取消息长度
  40. $lenList = unpack("N", substr($content, 0, 4));
  41. $jsonLen = $lenList[1];
  42. // 6. 获取消息 JSON
  43. $json = substr($content, 4, $jsonLen);
  44. return json_decode($json, true);
  45. }
  46. /**
  47. * 加密返回给钉钉的消息
  48. *
  49. * @param string $replyMsg 明文消息
  50. * @param string $timestamp
  51. * @param string $nonce
  52. * @return array
  53. */
  54. public function encryptMsg(string $replyMsg, string $timestamp, string $nonce): array
  55. {
  56. $random16 = $this->getRandomStr(16);
  57. $msgLenBin = pack("N", strlen($replyMsg));
  58. $corpId = ''; // 不需要corpId
  59. $plain = $random16 . $msgLenBin . $replyMsg . $corpId;
  60. // PKCS#7 padding
  61. $blockSize = 32;
  62. $pad = $blockSize - (strlen($plain) % $blockSize);
  63. $pad = $pad === 0 ? $blockSize : $pad;
  64. $plainPadded = $plain . str_repeat(chr($pad), $pad);
  65. // AES 加密
  66. $aesKey = base64_decode($this->encodingAesKey);
  67. $iv = substr($aesKey, 0, 16);
  68. $encrypted = openssl_encrypt($plainPadded, 'AES-256-CBC', $aesKey, OPENSSL_ZERO_PADDING, $iv);
  69. $encryptBase64 = base64_encode($encrypted);
  70. // 计算 msg_signature
  71. $signatureStr = $this->token . $timestamp . $nonce . $encryptBase64;
  72. $msgSignature = sha1($signatureStr);
  73. return [
  74. 'msg_signature' => $msgSignature,
  75. 'encrypt' => $encryptBase64
  76. ];
  77. }
  78. private function getRandomStr(int $length = 16): string
  79. {
  80. $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  81. $str = '';
  82. for ($i = 0; $i < $length; $i++) {
  83. $str .= $chars[random_int(0, strlen($chars) - 1)];
  84. }
  85. return $str;
  86. }
  87. private function pkcs7Unpad(string $text)
  88. {
  89. $pad = ord(substr($text, -1));
  90. if ($pad < 1 || $pad > 32) return false;
  91. return substr($text, 0, -$pad);
  92. }
  93. }