PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | vendor/wp-php-toolkit/reprint-server/src/class-hmac-client.php +139 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,139 @@
1 +<?php
2 +
3 +use function WordPress\Reprint\Server\generate_random_bytes;
4 +
5 +require_once __DIR__ . '/utils.php';
6 +
7 +/**
8 + * HMAC Client for the Site Export API.
9 + *
10 + * This class generates the required HMAC signatures for authenticating
11 + * requests to the Site Export API. The importing side uses this to sign
12 + * all outgoing requests.
13 + *
14 + * Usage:
15 + * $client = new Site_Export_HMAC_Client($shared_secret);
16 + * $headers = $client->get_auth_headers($request_body);
17 + * // Add $headers to your HTTP request
18 + *
19 + */
20 +class Site_Export_HMAC_Client {
21 +
22 + /**
23 + * Value of the X-Auth-Content-Hash header when the request body is
24 + * deliberately not signed: this literal string stands where a body hash
25 + * would otherwise be. Must match Site_Export_HMAC_Server::UNSIGNED_PAYLOAD.
26 + */
27 + public const UNSIGNED_PAYLOAD = 'UNSIGNED-PAYLOAD';
28 +
29 + /** @var string */
30 + private $secret;
31 +
32 + public function __construct(string $secret) {
33 + $this->secret = $secret;
34 + }
35 +
36 + /** @return string Hex-encoded 16-byte nonce. */
37 + public function generate_nonce(): string {
38 + return bin2hex(generate_random_bytes(16));
39 + }
40 +
41 + /** @return string Microsecond-precision Unix timestamp. */
42 + public function get_timestamp(): string {
43 + return sprintf('%.6f', microtime(true));
44 + }
45 +
46 + /**
47 + * Compute the HMAC signature for a request.
48 + *
49 + * The signature covers a SHA-256 hash of the body rather than the raw
50 + * bytes. This avoids having to predict the exact encoding that libcurl
51 + * will produce for multipart/form-data uploads while still binding the
52 + * request to a digest: the server verifies the timestamp, nonce, and HMAC
53 + * over X-Auth-Content-Hash before it computes or compares any body hash.
54 + *
55 + * This is intended for small command requests such as preflight or plan
56 + * confirmation. Large data transfers
57 + * should use an authenticated session and per-chunk hashes instead of
58 + * HMAC-signing one large request body.
59 + *
60 + * Signature = HMAC-SHA256(nonce + timestamp + SHA256(body), secret)
61 + *
62 + * @param string $nonce Random nonce for this request
63 + * @param string $timestamp Request timestamp
64 + * @param string $content_hash Hex SHA-256 hash of the request body
65 + * @return string Hex-encoded HMAC signature
66 + */
67 + public function compute_signature(string $nonce, string $timestamp, string $content_hash = ''): string {
68 + if ($content_hash === '') {
69 + $content_hash = hash('sha256', '');
70 + }
71 + $message = $nonce . $timestamp . $content_hash;
72 + return hash_hmac('sha256', $message, $this->secret);
73 + }
74 +
75 + /** Returns all four X-Auth-* headers for a single request. */
76 + public function get_auth_headers(string $body = ''): array {
77 + $nonce = $this->generate_nonce();
78 + $timestamp = $this->get_timestamp();
79 + $content_hash = hash('sha256', $body);
80 + $signature = $this->compute_signature($nonce, $timestamp, $content_hash);
81 +
82 + return [
83 + 'X-Auth-Signature' => $signature,
84 + 'X-Auth-Nonce' => $nonce,
85 + 'X-Auth-Timestamp' => $timestamp,
86 + 'X-Auth-Content-Hash' => $content_hash,
87 + ];
88 + }
89 +
90 + /**
91 + * Returns X-Auth-* headers for a request whose body is not signed.
92 + *
93 + * The signature covers the nonce, the timestamp, the method, and the
94 + * request target instead of a body hash, so a body of any size streams
95 + * through without either side hashing it, and captured auth headers still cannot be reused for a
96 + * different endpoint or method. Protecting the body from tampering is
97 + * TLS's job — over --force-http a tampered body would be accepted,
98 + * which is what that flag's help text warns about.
99 + *
100 + * Signature = HMAC-SHA256(nonce + timestamp + "UNSIGNED-PAYLOAD\n" + METHOD + "\n" + target, secret)
101 + *
102 + * @param string $method Uppercased into the signature (GET, POST, ...).
103 + * @param string $url Full request URL; only path and query are signed.
104 + */
105 + public function get_envelope_auth_headers(string $method, string $url): array {
106 + $nonce = $this->generate_nonce();
107 + $timestamp = $this->get_timestamp();
108 + $message = $nonce . $timestamp . self::UNSIGNED_PAYLOAD . "\n" . strtoupper($method) . "\n" . self::request_target($url);
109 +
110 + return [
111 + 'X-Auth-Signature' => hash_hmac('sha256', $message, $this->secret),
112 + 'X-Auth-Nonce' => $nonce,
113 + 'X-Auth-Timestamp' => $timestamp,
114 + 'X-Auth-Content-Hash' => self::UNSIGNED_PAYLOAD,
115 + ];
116 + }
117 +
118 + /**
119 + * Normalizes a URL to the "path?query" form both sides sign — the same
120 + * shape PHP exposes as $_SERVER['REQUEST_URI'] on the receiving end.
121 + */
122 + public static function request_target(string $url): string {
123 + $path = parse_url($url, PHP_URL_PATH);
124 + $query = parse_url($url, PHP_URL_QUERY);
125 + $target = is_string($path) && $path !== '' ? $path : '/';
126 +
127 + return is_string($query) && $query !== '' ? $target . '?' . $query : $target;
128 + }
129 +
130 + /** Returns auth headers formatted for CURLOPT_HTTPHEADER (["Name: value", ...]). */
131 + public function get_curl_headers(string $body = ''): array {
132 + $headers = $this->get_auth_headers($body);
133 + $curl_headers = [];
134 + foreach ($headers as $name => $value) {
135 + $curl_headers[] = "{$name}: {$value}";
136 + }
137 + return $curl_headers;
138 + }
139 +}