如何在PHP中解密Mastodon Web Push通知
Mastodon PHP Web Push通知解密方案
问题背景
已通过Mastodon订阅API创建Web Push订阅,POST数据如下:
$post_data = array( "subscription" => array( "endpoint" => $env["endpoint"], // PHP脚本所在的端点URL "keys" => array( "p256dh" => $env["vapid_public"], // 公钥 "auth" => $env["vapid_private"] // 私钥 ) ), "data" => array( "alerts" => array( "mention" => true // 接收提及通知 ) ) );
收到Mastodon推送通知时,通过file_get_contents("php://input")获取到二进制Payload,需解密但无法将请求头密钥、订阅密钥对应到openssl_decrypt()参数中。
收到的请求头(敏感值已替换):
[Authorization] => WebPush some_key_1.some_key_2.some_key_3 // JSON Web Token [Crypto-Key] => dh=crypto_key_1;p256ecdsa=crypto_key_2 [Encryption] => salt=salt_key [Content-Encoding] => aesgcm [Urgency] => normal [Ttl] => 172800 [Content-Type] => application/octet-stream [Digest] => SHA-256=digest_key [Accept-Encoding] => gzip [Date] => Wed, 22 Nov 2023 16:02:27 GMT [User-Agent] => http.rb/5.1.1 (Mastodon/4.2.1; +https://hidden_url.com/) [Content-Length] => 334 [Connection] => close [Host] => other_hidden_url.com [X-Real-Port] => xxxxx [X-Port] => 443 [X-Https] => on [X-Real-Ip] => hidden_ip [X-Forwarded-By] => other_hidden_ip
openssl_decrypt()函数参数:
function openssl_decrypt( string $data, string $cipher_algo, string $passphrase, int $options = 0, string $iv = "", string|null $tag = null, string $aad = "" ): string|false
解密步骤
Web Push AES-GCM解密需遵循Web Push协议规范,具体实现如下:
1. 解析请求头关键参数
- 从
Crypto-Key头提取dh值(对方公钥,base64url编码) - 从
Encryption头提取salt值(base64url编码) - 确认
Content-Encoding为aesgcm,对应加密算法为aes-128-gcm
2. 编码转换:base64url转标准base64
Web Push使用base64url编码,需先转换为标准base64再解码为二进制:
// 工具函数:base64url转标准base64 function base64url_to_base64($base64url) { $base64 = strtr($base64url, '-_', '+/'); return str_pad($base64, strlen($base64) + (4 - strlen($base64) % 4) % 4, '='); } // 解析salt $salt = base64_decode(base64url_to_base64(explode('=', $_SERVER['Encryption'])[1])); // 解析对方公钥dh $dh_value = explode(';', $_SERVER['Crypto-Key'])[0]; $remote_public_key = base64_decode(base64url_to_base64(explode('=', $dh_value)[1])); // 订阅时的本地私钥、公钥 $local_private_key = base64_decode(base64url_to_base64($env["vapid_private"])); $local_public_key = base64_decode(base64url_to_base64($env["vapid_public"]));
3. 生成共享密钥与派生密钥
用ECDH算法计算共享密钥,再通过HKDF派生解密所需的密钥和IV:
// 创建ECDH上下文,使用P-256曲线 $ecdh = openssl_pkey_new([ 'curve_name' => 'prime256v1', 'private_key_type' => OPENSSL_KEYTYPE_EC, ]); // 导入本地私钥 openssl_pkey_import($ecdh, $local_private_key); // 计算共享密钥 openssl_pkey_derive($ecdh, $remote_public_key, $shared_secret, 32); // HKDF派生函数 function hkdf($salt, $ikm, $info, $length) { $prk = hash_hmac('sha256', $ikm, $salt, true); $t = ''; $last = ''; $i = 1; while (strlen($t) < $length) { $last = hash_hmac('sha256', $last . $info . chr($i), $prk, true); $t .= $last; $i++; } return substr($t, 0, $length); } // 派生加密密钥(16字节) $encryption_key = hkdf($salt, $shared_secret, 'Content-Encoding: aesgcm' . chr(0), 16); // 派生IV(12字节) $iv = hkdf($salt, $shared_secret, 'Content-Encoding: nonce' . chr(0), 12);
4. 拆分Payload中的密文与Tag
Web Push AES-GCM Payload结构为:密文 + 16字节Auth Tag,需拆分处理:
$payload = file_get_contents('php://input'); $ciphertext = substr($payload, 0, -16); $tag = substr($payload, -16);
5. 调用openssl_decrypt解密
指定OPENSSL_RAW_DATA选项(数据为二进制):
$decrypted = openssl_decrypt( $ciphertext, 'aes-128-gcm', $encryption_key, OPENSSL_RAW_DATA, $iv, $tag, '' // Web Push中AAD字段为空 ); if ($decrypted === false) { die('解密失败:' . openssl_error_string()); } // 解密后为JSON格式,可解码使用 $notification = json_decode($decrypted, true); print_r($notification);
关键注意事项
- 所有base64url编码必须转换为标准base64后解码,否则密钥无效
- ECDH必须使用P-256曲线(prime256v1),这是Web Push标准曲线
- HKDF的info参数需严格按协议指定,末尾必须加
chr(0) - Payload必须拆分出16字节的Tag,否则解密失败
内容的提问来源于stack exchange,提问作者Marcel Bootsman
相关产品推荐
相关产品推荐

