PluginProbe ʕ •ᴥ•ʔ
UpdraftPlus: WP Backup & Migration Plugin / 1.13.15
UpdraftPlus: WP Backup & Migration Plugin v1.13.15
1.26.4 1.26.3 1.9.19 1.9.25 1.9.26 1.9.30 1.9.31 1.9.32 1.9.4 1.9.40 1.9.41 1.9.42 1.9.43 1.9.44 1.9.45 1.9.46 1.9.5 1.9.50 1.9.51 1.9.60 1.9.62 1.9.63 1.9.64 1.11.12 1.4.8 1.11.15 1.4.9 1.11.17 1.5.16 1.11.18 1.5.20 1.11.2 1.5.21 1.11.20 1.5.22 1.11.23 1.5.5 1.11.24 1.5.6 1.11.25 1.5.7 1.11.26 1.5.8 1.11.27 1.5.9 1.11.28 1.6.1 1.11.3 1.6.17 1.11.4 1.6.2 1.11.5 1.6.46 1.11.8 1.7.0 1.11.9 1.7.1 1.12.0 1.7.18 1.12.1 1.7.20 1.12.12 1.7.3 1.12.13 1.7.34 1.12.15 1.7.35 1.12.17 1.7.39 1.12.2 1.7.40 1.12.20 1.7.41 1.12.23 1.8.1 1.12.24 1.8.11 1.12.25 1.8.12 1.12.28 1.8.13 1.12.29 1.8.2 1.12.30 1.8.5 1.12.32 1.8.8 1.12.34 1.9.0 1.12.35 1.9.13 1.12.37 1.9.15 1.12.39 1.9.17 1.12.4 1.12.40 1.12.6 1.13.1 1.13.11 1.13.12 1.13.15 1.13.16 1.13.2 1.13.3 1.13.4 1.13.5 1.13.6 1.13.7 1.13.8 1.13.9 1.14.10 1.14.11 1.14.12 1.14.13 1.14.2 1.14.3 1.14.4 1.14.5 1.14.7 1.14.9 1.15.0 1.15.2 1.15.3 1.15.5 1.15.6 1.15.7 1.16.0 1.16.10 1.16.11 1.16.12 1.16.13 1.16.14 1.16.15 1.16.16 1.16.17 1.16.20 1.16.21 1.16.22 1.16.23 1.16.24 1.16.25 1.16.26 1.16.28 1.16.29 1.16.32 1.16.34 1.16.35 1.16.36 1.16.37 1.16.4 1.16.40 1.16.41 1.16.42 1.16.43 1.16.44 1.16.45 1.16.46 1.16.47 1.16.48 1.16.49 1.16.5 1.16.50 1.16.51 1.16.53 1.16.55 1.16.56 1.16.59 1.16.6 1.16.60 1.16.61 1.16.62 1.16.63 1.16.64 1.16.65 1.16.66 1.16.67 1.16.68 1.16.69 1.16.7 1.16.8 1.16.9 1.2.0 1.2.1 1.2.10 1.2.11 1.2.12 1.2.14 1.2.15 1.2.16 1.2.17 1.2.19 1.2.2 1.2.20 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.33 1.2.35 1.2.36 1.2.38 1.2.39 1.2.4 1.2.40 1.2.41 1.2.42 1.2.43 1.2.44 1.2.45 1.2.46 1.2.5 1.2.7 1.2.8 1.2.9 1.22.1 1.22.10 1.22.11 1.22.12 1.22.14 1.22.15 1.22.16 1.22.17 1.22.18 1.22.19 1.22.20 1.22.21 1.22.22 1.22.23 1.22.24 1.22.3 1.22.4 1.22.5 1.22.6 1.22.7 1.22.8 1.22.9 1.23.1 1.23.10 1.23.11 1.23.12 1.23.13 1.23.15 1.23.16 1.23.2 1.23.3 1.23.4 1.23.5 1.23.6 1.23.7 1.23.8 1.23.9 1.24.1 1.24.10 1.24.11 1.24.12 1.24.2 trunk 1.24.3 0.7.4 1.24.4 0.7.7 1.24.5 0.8.28 1.24.6 0.8.29 1.24.7 0.8.30 1.24.8 0.8.31 1.24.9 0.8.32 1.25.1 0.8.33 1.25.2 0.8.36 1.25.3 0.8.37 1.25.5 0.8.50 1.25.6 0.8.51 1.25.7 0.9.1 1.25.8 0.9.10 1.25.9 0.9.11 1.26.1 0.9.12 1.26.2 0.9.2 1.3.10 0.9.20 1.3.12 0.9.21 1.3.14 0.9.22 1.3.15 1.0.10 1.3.17 1.0.11 1.3.18 1.0.12 1.3.19 1.0.15 1.3.2 1.0.16 1.3.20 1.0.18 1.3.22 1.0.20 1.3.23 1.0.3 1.3.24 1.0.4 1.3.25 1.0.5 1.3.3 1.0.6 1.3.4 1.0.7 1.3.6 1.0.8 1.3.7 1.0.9 1.3.8 1.1.0 1.3.9 1.1.10 1.4.0 1.1.11 1.4.10 1.1.12 1.4.11 1.1.13 1.4.12 1.1.14 1.4.13 1.1.15 1.4.14 1.1.16 1.4.15 1.1.17 1.4.2 1.1.2 1.4.27 1.1.3 1.4.28 1.1.5 1.4.29 1.1.6 1.4.30 1.1.8 1.4.4 1.1.9 1.4.48 1.10.1 1.4.5 1.10.3 1.4.6 1.11.1 1.4.7
updraftplus / includes / class-udrpc.php
updraftplus / includes Last commit date
Dropbox2 8 years ago Google 9 years ago cloudfiles 12 years ago handlebars 8 years ago images 9 years ago jquery.serializeJSON 8 years ago jstree 8 years ago labelauty 8 years ago S3.php 8 years ago S3compat.php 8 years ago cacert.pem 9 years ago class-backup-history.php 8 years ago class-commands.php 8 years ago class-database-utility.php 8 years ago class-partialfileservlet.php 9 years ago class-semaphore.php 8 years ago class-udrpc.php 8 years ago class-updraftcentral-updraftplus-commands.php 8 years ago class-wpadmin-commands.php 8 years ago deprecated-actions.php 8 years ago ftp.class.php 8 years ago get-cpanel-quota-usage.pl 12 years ago google-extensions.php 9 years ago jquery-ui.custom.css 8 years ago jquery-ui.custom.min.css 8 years ago jquery-ui.custom.min.css.map 8 years ago jquery.blockUI.js 8 years ago jquery.blockUI.min.js 8 years ago updraft-notices.php 8 years ago updraftplus-admin.js 8 years ago updraftplus-admin.min.js 8 years ago updraftplus-notices.php 8 years ago updraftvault.php 8 years ago
class-udrpc.php
1099 lines
1 <?php
2 // @codingStandardsIgnoreStart
3 /*
4 This class provides methods for encrypting, sending, receiving and decrypting messages of arbitrary length, using standard encryption methods and including protection against replay attacks.
5
6 Example:
7
8 // Set a key and encrypt with it
9 $ud_rpc = new UpdraftPlus_Remote_Communications($name_indicator); // $name_indicator is a key indicator - indicating which key is being used.
10 $ud_rpc->set_key_local($our_private_key);
11 $ud_rpc->set_key_remote($their_public_key);
12 $encrypted = $ud_rpc->encrypt_message('blah blah');
13
14 // Use the saved WP site option
15 $ud_rpc = new UpdraftPlus_Remote_Communications($name_indicator); // $name_indicator is a key indicator - indicating which key is being used.
16 $ud_rpc->set_option_name('udrpc_remotekey');
17 if (!$ud_rpc->get_key_remote()) throw new Exception('...');
18 $encrypted = $ud_rpc->encrypt_message('blah blah');
19
20 // Generate a new key
21 $ud_rpc = new UpdraftPlus_Remote_Communications('myindicator.example.com');
22 $ud_rpc->set_option_name('udrpc_localkey'); // Save as a WP site option
23 $new_pair = $ud_rpc->generate_new_keypair();
24 if ($new_pair) {
25 $local_private_key = $ud_rpc->get_key_local();
26 $remote_public_key = $ud_rpc->get_key_remote();
27 // ...
28 } else {
29 throw new Exception('...');
30 }
31
32 // Send a message
33 $ud_rpc->activate_replay_protection();
34 $ud_rpc->set_destination_url('https://example.com/path/to/wp');
35 $ud_rpc->send_message('ping');
36 $ud_rpc->send_message('somecommand', array('param1' => 'data', 'param2' => 'moredata'));
37
38 // N.B. The data sent needs to be something that will pass json_encode(). So, it may be desirable to base64-encode it first.
39
40 // Create a listener for incoming messages
41
42 add_filter('udrpc_command_somecommand', 'my_function', 10, 3);
43 // function my_function($response, $data, $name_indicator) { ... ; return array('response' => 'my_reply', 'data' => 'any mixed data'); }
44 // Or:
45 // add_filter('udrpc_action', 'some_function', 10, 4); // Function must return something other than false to indicate that it handled the specific command. Any returned value will be sent as the reply.
46 // function some_function($response, $command, $data, $name_indicator) { ...; return array('response' => 'my_reply', 'data' => 'any mixed data'); }
47 $ud_rpc->set_option_name('udrpc_local_private_key');
48 $ud_rpc->activate_replay_protection();
49 if ($ud_rpc->get_key_local()) {
50 // Make sure you call this before the wp_loaded action is fired (e.g. at init)
51 $ud_rpc->create_listener();
52 }
53
54 // Instead of using activate_replay_protection(), you can use activate_sequence_protection() (receiving side) and set_next_send_sequence_id(). They are very similar; but, the sequence number code isn't tested, and is problematic if you may have multiple clients that don't share storage (you can use the current time as a sequence number, but if two clients send at the same millisecond (or whatever granularity you use), you may have problems); whereas the replay protection code relies on database storage on the sending side (not just the receiving).
55
56 */
57 // @codingStandardsIgnoreEnd
58 if (!class_exists('UpdraftPlus_Remote_Communications')) :
59 class UpdraftPlus_Remote_Communications {
60
61 // Version numbers relate to versions of this PHP library only (i.e. it's not a protocol support number, and version numbers of other compatible libraries (e.g. JavaScript) are not comparable)
62 public $version = '1.4.14';
63
64 private $key_name_indicator;
65
66 private $key_option_name = false;
67
68 private $key_remote = false;
69
70 private $key_local = false;
71
72 private $can_generate = false;
73
74 private $destination_url = false;
75
76 private $maximum_replay_time_difference = 300;
77
78 private $extra_replay_protection = false;
79
80 private $sequence_protection_tolerance;
81
82 private $sequence_protection_table;
83
84 private $sequence_protection_column;
85
86 private $sequence_protection_where_sql;
87
88 // Debug may log confidential data using $this->log() - so only use when you are in a secure environment
89 private $debug = false;
90
91 private $next_send_sequence_id;
92
93 private $allow_cors_from = array();
94
95 private $http_transport = null;
96
97 // Default protocol version - this can be over-ridden with set_message_format
98 // Protocol version 1 (which uses only one RSA key-pair, instead of two) is legacy/deprecated
99 private $format = 2;
100
101 private $http_credentials = array();
102
103 private $incoming_message = null;
104
105 private $message_random_number = null;
106
107 private $require_message_to_be_understood = false;
108
109 public function __construct($key_name_indicator = 'default', $can_generate = false) {
110 $this->set_key_name_indicator($key_name_indicator);
111 }
112
113 public function set_key_name_indicator($key_name_indicator) {
114 $this->key_name_indicator = $key_name_indicator;
115 }
116
117 public function set_can_generate($can_generate = true) {
118 $this->can_generate = $can_generate;
119 }
120
121 /**
122 * Which sites to allow CORS requests from
123 *
124 * @param string $allow_cors_from
125 */
126 public function set_allow_cors_from($allow_cors_from) {
127 $this->allow_cors_from = $allow_cors_from;
128 }
129
130 public function set_maximum_replay_time_difference($replay_time_difference) {
131 $this->maximum_replay_time_difference = (int) $replay_time_difference;
132 }
133
134 /**
135 * This will cause more things to be sent to $this->log()
136 *
137 * @param boolean $debug
138 */
139 public function set_debug($debug = true) {
140 $this->debug = (bool) $debug;
141 }
142
143 /**
144 * Supported values: a Guzzle object, or, if not, then WP's HTTP API function siwll be used
145 *
146 * @param string $transport
147 */
148 public function set_http_transport($transport) {
149 $this->http_transport = $transport;
150 }
151
152 /**
153 * Sequence protection and replay protection perform similar functions, and using both is often over-kill; the distinction is that sequence protection can be used without needing to do database writes on the sending side (e.g. use the value of time() as the sequence number).
154 * The only rule of sequences is that the receiving side will reject any sequence number that is less than the last previously seen one, within the bounds of the tolerance (but it may also reject those if they are repeats).
155 * The given table/column will record a comma-separated list of recently seen sequences numbers within the tolerance threshold.
156 *
157 * @param string $table
158 * @param string $column
159 * @param string $where_sql
160 * @param integer $tolerance
161 */
162 public function activate_sequence_protection($table, $column, $where_sql, $tolerance = 5) {
163 $this->sequence_protection_tolerance = (int) $tolerance;
164 $this->sequence_protection_table = (string) $table;
165 $this->sequence_protection_column = (string) $column;
166 $this->sequence_protection_where_sql = (string) $where_sql;
167 }
168
169 private function ensure_crypto_loaded() {
170 if (!class_exists('Crypt_Rijndael') || !class_exists('Crypt_RSA') || !class_exists('Crypt_Hash')) {
171 global $updraftplus;
172 // phpseclib 1.x uses deprecated PHP4-style constructors
173 $this->no_deprecation_warnings_on_php7();
174 if (is_a($updraftplus, 'UpdraftPlus')) {
175 $updraftplus->ensure_phpseclib(array('Crypt_Rijndael', 'Crypt_RSA', 'Crypt_Hash'), array('Crypt/Rijndael', 'Crypt/RSA', 'Crypt/Hash'));
176 } elseif (defined('UPDRAFTPLUS_DIR') && file_exists(UPDRAFTPLUS_DIR.'/vendor/phpseclib/phpseclib/phpseclib')) {
177 $pdir = UPDRAFTPLUS_DIR.'/vendor/phpseclib/phpseclib/phpseclib';
178 if (false === strpos(get_include_path(), $pdir)) set_include_path($pdir.PATH_SEPARATOR.get_include_path());
179 if (!class_exists('Crypt_Rijndael')) include_once 'Crypt/Rijndael.php';
180 if (!class_exists('Crypt_RSA')) include_once 'Crypt/RSA.php';
181 if (!class_exists('Crypt_Hash')) include_once 'Crypt/Hash.php';
182 } elseif (file_exists(dirname(dirname(__FILE__)).'/vendor/phpseclib/phpseclib/phpseclib')) {
183 $pdir = dirname(dirname(__FILE__)).'/vendor/phpseclib/phpseclib/phpseclib';
184 if (false === strpos(get_include_path(), $pdir)) set_include_path($pdir.PATH_SEPARATOR.get_include_path());
185 if (!class_exists('Crypt_Rijndael')) include_once 'Crypt/Rijndael.php';
186 if (!class_exists('Crypt_RSA')) include_once 'Crypt/RSA.php';
187 if (!class_exists('Crypt_Hash')) include_once 'Crypt/Hash.php';
188 }
189 }
190 }
191
192 /**
193 * Ugly, but necessary to prevent debug output breaking the conversation when the user has debug turned on
194 */
195 private function no_deprecation_warnings_on_php7() {
196 // PHP_MAJOR_VERSION is defined in PHP 5.2.7+
197 // We don't test for PHP > 7 because the specific deprecated element will be removed in PHP 8 - and so no warning should come anyway (and we shouldn't suppress other stuff until we know we need to).
198 if (defined('PHP_MAJOR_VERSION') && PHP_MAJOR_VERSION == 7) {
199 $old_level = error_reporting();
200 // @codingStandardsIgnoreLine
201 $new_level = $old_level & ~E_DEPRECATED;
202 if ($old_level != $new_level) error_reporting($new_level);
203 }
204 }
205
206 public function set_destination_url($destination_url) {
207 $this->destination_url = $destination_url;
208 }
209
210 public function get_destination_url() {
211 return $this->destination_url;
212 }
213
214 public function set_option_name($key_option_name) {
215 $this->key_option_name = $key_option_name;
216 }
217
218 /**
219 * Method to get the remote key
220 *
221 * @return array
222 */
223 public function get_key_remote() {
224 if (empty($this->key_remote) && $this->can_generate) {
225 $this->generate_new_keypair();
226 }
227
228 return empty($this->key_remote) ? false : $this->key_remote;
229 }
230
231 /**
232 * Set the remote key
233 *
234 * @param string $key_remote
235 */
236 public function set_key_remote($key_remote) {
237 $this->key_remote = $key_remote;
238 }
239
240 /**
241 * Used for sending - when receiving, the format is part of the message
242 *
243 * @param integer $format
244 */
245 public function set_message_format($format = 2) {
246 $this->format = $format;
247 }
248
249 /**
250 * Method to get the local key
251 *
252 * @return array
253 */
254 public function get_key_local() {
255 if (empty($this->key_local)) {
256 if ($this->key_option_name) {
257 $key_local = get_site_option($this->key_option_name);
258 if ($key_local) {
259 $this->key_local = $key_local;
260 }
261 }
262 }
263 if (empty($this->key_local) && $this->can_generate) {
264 $this->generate_new_keypair();
265 }
266
267 return empty($this->key_local) ? false : $this->key_local;
268 }
269
270 /**
271 * Tests whether a supplied string (after trimming) is a valid portable bundle
272 *
273 * @param string $bundle [description]
274 * @param string $format same as get_portable_bundle()
275 * @return array (which the consumer is free to use - e.g. convert into internationalised string), with keys 'code' and (perhaps) 'data'
276 */
277 public function decode_portable_bundle($bundle, $format = 'raw') {
278 $bundle = trim($bundle);
279 if ('base64_with_count' == $format) {
280 if (strlen($bundle) < 5) return array('code' => 'invalid_wrong_length', 'data' => 'too_short');
281 $len = substr($bundle, 0, 4);
282 $bundle = substr($bundle, 4);
283 $len = hexdec($len);
284 if (strlen($bundle) != $len) return array('code' => 'invalid_wrong_length', 'data' => "1,$len,".strlen($bundle));
285 if (false === ($bundle = base64_decode($bundle))) return array('code' => 'invalid_corrupt', 'data' => 'not_base64');
286 if (null === ($bundle = json_decode($bundle, true))) return array('code' => 'invalid_corrupt', 'data' => 'not_json');
287 }
288 if (empty($bundle['key'])) return array('code' => 'invalid_corrupt', 'data' => 'no_key');
289 if (empty($bundle['url'])) return array('code' => 'invalid_corrupt', 'data' => 'no_url');
290 if (empty($bundle['name_indicator'])) return array('code' => 'invalid_corrupt', 'data' => 'no_name_indicator');
291
292 return $bundle;
293 }
294
295 /**
296 * Method to get a portable bundle sufficient to contact this site (i.e. remote site - so you need to have generated a key-pair, or stored the remote key somewhere and restored it)
297 *
298 * @param string $format Supported formats: base64_with_count and default)raw
299 * @param array $extra_info needs to be JSON-serialisable, so be careful about what you put into it.
300 * @param array $options [description]
301 * @return array
302 */
303 public function get_portable_bundle($format = 'raw', $extra_info = array(), $options = array()) {
304
305 $bundle = array_merge($extra_info, array(
306 'key' => empty($options['key']) ? $this->get_key_remote() : $options['key'],
307 'name_indicator' => $this->key_name_indicator,
308 'url' => trailingslashit(network_site_url()),
309 'admin_url' => trailingslashit(admin_url()),
310 'network_admin_url' => trailingslashit(network_admin_url()),
311 ));
312
313 if ('base64_with_count' == $format) {
314 $bundle = base64_encode(json_encode($bundle));
315
316 $len = strlen($bundle); // Get the length
317 $len = dechex($len); // The first bytes of the message are the bundle length
318 $len = str_pad($len, 4, '0', STR_PAD_LEFT); // Zero pad
319
320 return $len.$bundle;
321
322 } else {
323 return $bundle;
324 }
325
326 }
327
328 public function set_key_local($key_local) {
329 $this->key_local = $key_local;
330 if ($this->key_option_name) update_site_option($this->key_option_name, $this->key_local);
331 }
332
333 public function generate_new_keypair($key_size = 2048) {
334
335 $this->ensure_crypto_loaded();
336
337 $rsa = new Crypt_RSA();
338 $keys = $rsa->createKey($key_size);
339
340 if (empty($keys['privatekey'])) {
341 $this->set_key_local(false);
342 } else {
343 $this->set_key_local($keys['privatekey']);
344 }
345
346 if (empty($keys['publickey'])) {
347 $this->set_key_remote(false);
348 } else {
349 $this->set_key_remote($keys['publickey']);
350 }
351
352 return empty($keys['publickey']) ? false : true;
353 }
354
355 /**
356 * A base-64 encoded RSA hash (PKCS_1) of the message digest
357 *
358 * @param string $message
359 * @param boolean $use_key
360 * @return array
361 */
362 public function signature_for_message($message, $use_key = false) {
363
364 $hash_algorithm = 'sha256';
365
366 // Sign with the private (local) key
367 if (!$use_key) {
368 if (!$this->key_local) throw new Exception('No signing key has been set');
369 $use_key = $this->key_local;
370 }
371
372 $this->ensure_crypto_loaded();
373
374 $rsa = new Crypt_RSA();
375 $rsa->loadKey($use_key);
376 // This is the older signature mode; phpseclib's default is the preferred CRYPT_RSA_SIGNATURE_PSS; however, Forge JS doesn't yet support this. More info: https://en.wikipedia.org/wiki/PKCS_1
377 $rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
378
379 // Don't do this: Crypt_RSA::sign() already calculates the digest of the hash
380 // $hash = new Crypt_Hash($hash_algorithm);
381 // $hashed = $hash->hash($message);
382
383 // if ($this->debug) $this->log("Message hash (hash=$hash_algorithm) (hex): ".bin2hex($hashed));
384
385 // phpseclib defaults to SHA1
386 $rsa->setHash($hash_algorithm);
387 $encrypted = $rsa->sign($message);
388
389 if ($this->debug) $this->log('Signed hash (mode='.CRYPT_RSA_SIGNATURE_PKCS1.') (hex): '.bin2hex($encrypted));
390
391 $signature = base64_encode($encrypted);
392
393 if ($this->debug) $this->log("Message signature (base64): $signature");
394
395 return $signature;
396 }
397
398 /**
399 * Log description
400 *
401 * @param string $message
402 * @param string $level $level is not yet used much
403 */
404 private function log($message, $level = 'notice') {
405 // Allow other plugins to do something with the message
406 do_action('udrpc_log', $message, $level, $this->key_name_indicator, $this->debug, $this);
407 if ('info' != $level) error_log('UDRPC ('.$this->key_name_indicator.", $level): $message");
408 }
409
410 /**
411 * Encrypt the message, using the local key (which needs to exist)
412 *
413 * @param string $plaintext
414 * @param boolean $use_key
415 * @param integer $key_length
416 * @return array
417 */
418 public function encrypt_message($plaintext, $use_key = false, $key_length = 32) {
419
420 if (!$use_key) {
421 if (1 == $this->format) {
422 if (!$this->key_local) throw new Exception('No encryption key has been set');
423 $use_key = $this->key_local;
424 } else {
425 if (!$this->key_remote) throw new Exception('No encryption key has been set');
426 $use_key = $this->key_remote;
427 }
428 }
429
430 $this->ensure_crypto_loaded();
431
432 $rsa = new Crypt_RSA();
433
434 if (defined('UDRPC_PHPSECLIB_ENCRYPTION_MODE')) $rsa->setEncryptionMode(UDRPC_PHPSECLIB_ENCRYPTION_MODE);
435
436 $rij = new Crypt_Rijndael();
437
438 // Generate Random Symmetric Key
439 $sym_key = crypt_random_string($key_length);
440
441 if ($this->debug) $this->log('Unencrypted symmetric key (hex): '.bin2hex($sym_key));
442
443 // Encrypt Message with new Symmetric Key
444 $rij->setKey($sym_key);
445 $ciphertext = $rij->encrypt($plaintext);
446
447 if ($this->debug) $this->log('Encrypted ciphertext (hex): '.bin2hex($ciphertext));
448
449 $ciphertext = base64_encode($ciphertext);
450
451 // Encrypt the Symmetric Key with the Asymmetric Key
452 $rsa->loadKey($use_key);
453 $sym_key = $rsa->encrypt($sym_key);
454
455 if ($this->debug) $this->log('Encrypted symmetric key (hex): '.bin2hex($sym_key));
456
457 // Base 64 encode the symmetric key for transport
458 $sym_key = base64_encode($sym_key);
459
460 if ($this->debug) $this->log('Encrypted symmetric key (b64): '.$sym_key);
461
462 $len = str_pad(dechex(strlen($sym_key)), 3, '0', STR_PAD_LEFT); // Zero pad to be sure.
463
464 // 16 characters of hex is enough for the payload to be to 16 exabytes (giga < tera < peta < exa) of data
465 $cipherlen = str_pad(dechex(strlen($ciphertext)), 16, '0', STR_PAD_LEFT);
466
467 // Concatenate the length, the encrypted symmetric key, and the message
468 return $len.$sym_key.$cipherlen.$ciphertext;
469
470 }
471
472 /**
473 * Decrypt the message, using the local key (which needs to exist)
474 *
475 * @param string $message
476 * @return array
477 */
478 public function decrypt_message($message) {
479
480 if (!$this->key_local) throw new Exception('No decryption key has been set');
481
482 $this->ensure_crypto_loaded();
483
484 $rsa = new Crypt_RSA();
485 if (defined('UDRPC_PHPSECLIB_ENCRYPTION_MODE')) $rsa->setEncryptionMode(UDRPC_PHPSECLIB_ENCRYPTION_MODE);
486 // Defaults to CRYPT_AES_MODE_CBC
487 $rij = new Crypt_Rijndael();
488
489 // Extract the Symmetric Key
490 $len = substr($message, 0, 3);
491 $len = hexdec($len);
492 $sym_key = substr($message, 3, $len);
493
494 // Extract the encrypted message
495 $cipherlen = substr($message, ($len + 3), 16);
496 $cipherlen = hexdec($cipherlen);
497
498 $ciphertext = substr($message, ($len + 19), $cipherlen);
499 $ciphertext = base64_decode($ciphertext);
500
501 // Decrypt the encrypted symmetric key
502 $rsa->loadKey($this->key_local);
503 $sym_key = base64_decode($sym_key);
504 $sym_key = $rsa->decrypt($sym_key);
505
506 // Decrypt the message
507 $rij->setKey($sym_key);
508
509 return $rij->decrypt($ciphertext);
510
511 }
512
513 /**
514 * Creates a message
515 *
516 * @param string $command
517 * @param string $data
518 * @param boolean $is_response
519 * @param boolean $use_key_remote
520 * @param boolean $use_key_local
521 * @return array which the caller will then format as required (e.g. use as body in post, or JSON-encode, etc.) [description]
522 */
523 public function create_message($command, $data = null, $is_response = false, $use_key_remote = false, $use_key_local = false) {
524
525 if ($is_response) {
526 $send_array = array('response' => $command);
527 } else {
528 $send_array = array('command' => $command);
529 }
530
531 $send_array['time'] = time();
532 // This goes in the encrypted portion as well to prevent replays with a different unencrypted name indicator
533 $send_array['key_name'] = $this->key_name_indicator;
534
535 // This random element means that if the site needs to send two identical commands or responses in the same second, then it can, and still use replay protection
536 // The value of PHP_INT_MAX on a 32-bit platform
537 $this->message_random_number = rand(1, 2147483647);
538 $send_array['rand'] = $this->message_random_number;
539
540 if ($this->next_send_sequence_id) {
541 $send_array['sequence_id'] = $this->next_send_sequence_id;
542 ++$this->next_send_sequence_id;
543 }
544
545 if ($is_response && !empty($this->incoming_message) && isset($this->incoming_message['rand'])) {
546 $send_array['incoming_rand'] = $this->incoming_message['rand'];
547 }
548
549 if (null !== $data) $send_array['data'] = $data;
550 $send_data = $this->encrypt_message(json_encode($send_array), $use_key_remote);
551
552 $message = array(
553 'format' => $this->format,
554 'key_name' => $this->key_name_indicator,
555 'udrpc_message' => $send_data,
556 );
557
558 if ($this->format >= 2) {
559 $signature = $this->signature_for_message($send_data, $use_key_local);
560 $message['signature'] = $signature;
561 }
562
563 return $message;
564
565 }
566
567 /**
568 * N.B. There's already some time-based replay protection. This can be turned on to beef it up.
569 * This is only for listeners. Replays can only be detection if transients are working on the WP site (which by default only means that the option table is working).
570 *
571 * @param boolean $activate
572 */
573 public function activate_replay_protection($activate = true) {
574 $this->extra_replay_protection = (bool) $activate;
575 }
576
577 public function set_next_send_sequence_id($id) {
578 $this->next_send_sequence_id = $id;
579 }
580
581 /**
582 * Set_http_credentials
583 *
584 * @param string $credentials should be an array with entries for 'username' and 'password'
585 */
586 public function set_http_credentials($credentials) {
587 $this->http_credentials = $credentials;
588 }
589
590 /**
591 * This needs only to return an array with keys body and response - where response is also an array, with key 'code' (the HTTP status code)
592 * The $post_options array support these keys: timeout, body,
593 * Public, to allow short-circuiting of the library's own encoding/decoding (e.g. for acting as a proxy for a message already encrypted elsewhere)
594 *
595 * @param array $post_options
596 * @return array
597 */
598 public function http_post($post_options) {
599 // @codingStandardsIgnoreLine
600 @include ABSPATH.WPINC.'/version.php';
601 $http_credentials = $this->http_credentials;
602
603 if (is_a($this->http_transport, 'GuzzleHttp\Client')) {
604
605 // https://guzzle.readthedocs.org/en/5.3/clients.html
606
607 $client = $this->http_transport;
608
609 $guzzle_options = array(
610 'body' => $post_options['body'],
611 'headers' => array(
612 'User-Agent' => 'WordPress/'.$wp_version.'; class-udrpc.php-Guzzle/'.$this->version.'; '.get_bloginfo('url'),
613 ),
614 'exceptions' => false,
615 'timeout' => $post_options['timeout'],
616 );
617
618 if (!class_exists('WP_HTTP_Proxy')) include_once ABSPATH.WPINC.'/class-http.php';
619 $proxy = new WP_HTTP_Proxy();
620 if ($proxy->is_enabled()) {
621 $user = $proxy->username();
622 $pass = $proxy->password();
623 $host = $proxy->host();
624 $port = (int) $proxy->port();
625 if (empty($port)) $port = 8080;
626 if (!empty($host) && $proxy->send_through_proxy($this->destination_url)) {
627 $proxy_auth = '';
628 if (!empty($user)) {
629 $proxy_auth = $user;
630 if (!empty($pass)) $proxy_auth .= ':'.$pass;
631 $proxy_auth .= '@';
632 }
633 $guzzle_options['proxy'] = array(
634 'http' => "http://${proxy_auth}$host:$port",
635 'https' => "http://${proxy_auth}$host:$port",
636 );
637 }
638 }
639
640 if (defined('UDRPC_GUZZLE_SSL_VERIFY')) {
641 $verify = UDRPC_GUZZLE_SSL_VERIFY;
642 } elseif (file_exists(ABSPATH.WPINC.'/certificates/ca-bundle.crt')) {
643 $verify = ABSPATH.WPINC.'/certificates/ca-bundle.crt';
644 } else {
645 $verify = true;
646 }
647 $guzzle_options['verify'] = apply_filters('udrpc_guzzle_verify', $verify);
648
649 if (!empty($http_credentials['username'])) {
650
651 $authentication_method = empty($http_credentials['authentication_method']) ? 'basic' : $http_credentials['authentication_method'];
652
653 $password = empty($http_credentials['password']) ? '' : $http_credentials['password'];
654
655 $guzzle_options['auth'] = array(
656 $http_credentials['username'],
657 $password,
658 $authentication_method,
659 );
660
661 }
662
663 $response = $client->post($this->destination_url, apply_filters('udrpc_guzzle_options', $guzzle_options, $this));
664
665 $formatted_response = array(
666 'response' => array(
667 'code' => $response->getStatusCode(),
668 ),
669 'body' => $response->getBody(),
670 );
671
672 return $formatted_response;
673
674 } else {
675
676 $post_options['user-agent'] = 'WordPress/'.$wp_version.'; class-udrpc.php/'.$this->version.'; '.get_bloginfo('url');
677
678 if (!empty($http_credentials['username'])) {
679
680 $authentication_type = empty($http_credentials['authentication_type']) ? 'basic' : $http_credentials['authentication_type'];
681
682 if ('basic' != $authentication_type) {
683 return new WP_Error('unsupported_http_authentication_type', 'Only HTTP basic authentication is supported (for other types, use Guzzle)');
684 }
685
686 $password = empty($http_credentials['password']) ? '' : $http_credentials['password'];
687 $post_options['headers'] = array(
688 'Authorization' => 'Basic '.base64_encode($http_credentials['username'].':'.$password),
689 );
690 }
691
692 return wp_remote_post(
693 $this->destination_url,
694 $post_options
695 );
696 }
697 }
698
699 public function send_message($command, $data = null, $timeout = 20) {
700
701 if (empty($this->destination_url)) return new WP_Error('not_initialised', 'RPC error: URL not initialised');
702
703 $message = $this->create_message($command, $data);
704
705 $post_options = array(
706 'timeout' => $timeout,
707 'body' => $message,
708 );
709
710 $post_options = apply_filters('udrpc_post_options', $post_options, $command, $data, $timeout, $this);
711
712 try {
713 $post = $this->http_post($post_options);
714 } catch (Exception $e) {
715 // Curl can return an error code 0, which causes WP_Error to return early, without recording the message. So, we prefix the code.
716 return new WP_Error('http_post_'.$e->getCode(), $e->getMessage());
717 }
718
719 if (is_wp_error($post)) return $post;
720
721 $response_code = wp_remote_retrieve_response_code($post);
722
723 if (empty($response_code)) return new WP_Error('empty_http_code', 'Unexpected HTTP response code');
724
725 if ($response_code < 200 || $response_code >= 300) return new WP_Error('unexpected_http_code', 'Unexpected HTTP response code ('.$response_code.')', $post);
726
727 $response_body = wp_remote_retrieve_body($post);
728
729 if (empty($response_body)) return new WP_Error('empty_response', 'Empty response from remote site');
730
731 $decoded = json_decode($response_body, true);
732
733 if (empty($decoded)) {
734
735 if (false != ($found_at = strpos($response_body, '{"format":'))) {
736 $new_body = substr($response_body, $found_at);
737 $decoded = json_decode($new_body, true);
738 }
739
740 if (empty($decoded)) {
741 $this->log('response from remote site could not be understood: '.substr($response_body, 0, 100).' ... ');
742
743 return new WP_Error('response_not_understood', 'Response from remote site could not be understood', $response_body);
744 }
745 }
746
747 if (!is_array($decoded) || empty($decoded['udrpc_message'])) return new WP_Error('response_not_understood', 'Response from remote site was not in the expected format ('.$post['body'].')', $decoded);
748
749 if ($this->format >= 2) {
750 if (empty($decoded['signature'])) {
751 $this->log('No message signature found');
752 die;
753 }
754 if (!$this->key_remote) {
755 $this->log('No signature verification key has been set');
756 die;
757 }
758 if (!$this->verify_signature($decoded['udrpc_message'], $decoded['signature'], $this->key_remote)) {
759 $this->log('Signature verification failed; discarding');
760 die;
761 }
762 }
763
764 $decoded = $this->decrypt_message($decoded['udrpc_message']);
765
766 if (!is_string($decoded)) return new WP_Error('not_decrypted', 'Response from remote site was not successfully decrypted', $decoded['udrpc_message']);
767
768 $json_decoded = json_decode($decoded, true);
769
770 if (!is_array($json_decoded) || empty($json_decoded['response']) || empty($json_decoded['time']) || !is_numeric($json_decoded['time'])) return new WP_Error('response_corrupt', 'Response from remote site was not in the expected format', $decoded);
771
772 // Don't do the reply detection until now, because $post['body'] may not be a message that originated from the remote component at all (e.g. an HTTP error)
773 if ($this->extra_replay_protection) {
774 $message_hash = $this->calculate_message_hash((string) $post['body']);
775 if ($this->message_hash_seen($message_hash)) {
776 return new WP_Error('replay_detected', 'Message refused: replay detected', $message_hash);
777 }
778 }
779
780 $time_difference = absint((time() - $json_decoded['time']));
781 if ($time_difference > $this->maximum_replay_time_difference) return new WP_Error('window_error', 'Message refused: maxium replay time difference exceeded', $time_difference);
782
783 if (isset($json_decoded['incoming_rand']) && !empty($this->message_random_number) && $json_decoded['incoming_rand'] != $this->message_random_number) {
784 // @codingStandardsIgnoreLine
785 $this->log('UDRPC: Message mismatch (possibly MITM) (sent_rand=' + $this->message_random_number + ', returned_rand='.$json_decoded['incoming_rand'].'): dropping', 'error');
786
787 return new WP_Error('message_mismatch_error', 'Message refused: message mismatch (possible MITM)');
788
789 }
790
791 // Should be an array with keys including 'response' and (if relevant) 'data'
792 return $json_decoded;
793
794 }
795
796 /**
797 * Returns a boolean indicating whether a listener was created - which depends on whether one was needed (so, false does not necessarily indicate an error condition)
798 *
799 * @return boolean
800 */
801 public function create_listener() {
802
803 $http_origin = function_exists('get_http_origin') ? get_http_origin() : (empty($_SERVER['HTTP_ORIGIN']) ? '' : $_SERVER['HTTP_ORIGIN']);
804
805 // Create the WP actions to handle incoming commands, handle built-in commands (e.g. ping, create_keys (authenticate with admin creds)), dispatch them to the right place, and die
806 if ((!empty($_POST) && !empty($_POST['udrpc_message']) && !empty($_POST['format'])) || (!empty($_SERVER['REQUEST_METHOD']) && 'OPTIONS' == $_SERVER['REQUEST_METHOD'] && $http_origin)) {
807 add_action('wp_loaded', array($this, 'wp_loaded'));
808 add_action('wp_loaded', array($this, 'wp_loaded_final'), 10000);
809 return true;
810 }
811
812 return false;
813 }
814
815 public function wp_loaded_final() {
816 if (empty($this->require_message_to_be_understood)) return;
817 $message_for = empty($_POST['key_name']) ? '' : (string) $_POST['key_name'];
818 $this->log("Message was received, but not understood by local site (for: $message_for)");
819 die;
820 }
821
822 public function wp_loaded() {
823
824 /*
825 // What if something else already set some response headers?
826 if (function_exists('apache_response_headers')) {
827 $apache_response_headers = apache_response_headers();
828 // Do something...
829 }
830 */
831
832 // CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
833 // get_http_origin() : since WP 3.4
834 $http_origin = function_exists('get_http_origin') ? get_http_origin() : (empty($_SERVER['HTTP_ORIGIN']) ? '' : $_SERVER['HTTP_ORIGIN']);
835 if (!empty($_SERVER['REQUEST_METHOD']) && 'OPTIONS' == $_SERVER['REQUEST_METHOD'] && $http_origin) {
836 if (in_array($http_origin, $this->allow_cors_from)) {
837 // @codingStandardsIgnoreLine
838 if (!@constant('UDRPC_DO_NOT_SEND_CORS_HEADERS')) {
839 header("Access-Control-Allow-Origin: $http_origin");
840 header('Access-Control-Allow-Credentials: true');
841 if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) header('Access-Control-Allow-Methods: POST, OPTIONS');
842 if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) header('Access-Control-Allow-Headers: '.$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']);
843 }
844 die;
845 } elseif ($this->debug) {
846 $this->log('Non-allowed CORS from: '.$http_origin);
847 }
848 // Having detected that this is a CORS request, there's nothing more to do. We return, because a different listener might pick it up, even though we didn't.
849 return;
850 }
851
852 // Silently return, rather than dying, in case another instance is able to handle this
853 if (empty($_POST['format']) || (1 != $_POST['format'] && 2 != $_POST['format'])) return;
854
855 $this->require_message_to_be_understood = true;
856
857 $format = $_POST['format'];
858
859 /*
860 In format 1 (legacy/obsolete), the one encrypts (the shared AES key) using one half of the key-pair, and decrypts with the other; whereas the other side of the conversation does the reverse when replying (and uses a different shared AES key). Though this is possible in RSA, this is the wrong thing to do - see https://crypto.stackexchange.com/questions/2123/rsa-encryption-with-private-key-and-decryption-with-a-public-key
861
862 In format 2, both sides have their own private and public key. The sender encrypts using the other side's public key, and decrypts using its own private key. Messages are signed (the message digest is SHA-256).
863
864 */
865
866 // Is this for us?
867 if (empty($_POST['key_name']) || $_POST['key_name'] != $this->key_name_indicator) {
868 return;
869 }
870
871 // wp_unslash() does not exist until after WP 3.5
872 // $udrpc_message = function_exists('wp_unslash') ? wp_unslash($_POST['udrpc_message']) : stripslashes_deep($_POST['udrpc_message']);
873
874 // Data should not have any slashes - it is base64-encoded
875 $udrpc_message = (string) $_POST['udrpc_message'];
876
877 // Check this now, rather than allow the decrypt method to thrown an Exception
878
879 if (empty($this->key_local)) {
880 $this->log('no local key (format 1): cannot decrypt', 'error');
881 die;
882 }
883
884 if ($format >= 2) {
885 if (empty($_POST['signature'])) {
886 $this->log('No message signature found', 'error');
887 die;
888 }
889 if (!$this->key_remote) {
890 $this->log('No signature verification key has been set', 'error');
891 die;
892 }
893 if (!$this->verify_signature($udrpc_message, $_POST['signature'], $this->key_remote)) {
894 $this->log('Signature verification failed; discarding', 'error');
895 die;
896 }
897 }
898
899 try {
900 $udrpc_message = $this->decrypt_message($udrpc_message);
901 } catch (Exception $e) {
902 $this->log('Exception ('.get_class($e).'): '.$e->getMessage(), 'error');
903 die;
904 }
905
906 $udrpc_message = json_decode($udrpc_message, true);
907
908 if (empty($udrpc_message) || !is_array($udrpc_message) || empty($udrpc_message['command']) || !is_string($udrpc_message['command'])) {
909 $this->log('Could not decode JSON on incoming message', 'error');
910 die;
911 }
912
913 if (empty($udrpc_message['time'])) {
914 $this->log('No time set in incoming message', 'error');
915 die;
916 }
917
918 // Mismatch indicating a replay of the message with a different key name in the unencrypted portion?
919 if (empty($udrpc_message['key_name']) || $_POST['key_name'] != $udrpc_message['key_name']) {
920 $this->log('key_name mismatch between encrypted and unencrypted portions', 'error');
921 die;
922 }
923
924 if ($this->extra_replay_protection) {
925 $message_hash = $this->calculate_message_hash((string) $_POST['udrpc_message']);
926 if ($this->message_hash_seen($message_hash)) {
927 $this->log("Message dropped: apparently a replay (hash: $message_hash)", 'error');
928 die;
929 }
930 }
931
932 // Do this after the extra replay protection, as that checks hashes within the maximum time window - so don't check the maximum time window until afterwards, to avoid a tiny window (race) in between.
933 $time_difference = absint(($udrpc_message['time'] - time()));
934 if ($time_difference > $this->maximum_replay_time_difference) {
935 $this->log("Time in incoming message is outside of allowed window ($time_difference > ".$this->maximum_replay_time_difference.')', 'error');
936 die;
937 }
938
939 // The sequence number should always be larger than any previously-sent sequence number
940 if ($this->sequence_protection_tolerance) {
941
942 if ($this->debug) $this->log('Sequence protection is active; tolerance: '.$this->sequence_protection_tolerance);
943
944 global $wpdb;
945
946 if (!isset($udrpc_message['sequence_id']) || !is_numeric($udrpc_message['sequence_id'])) {
947 $this->log('a numerical sequence number is required, but none was included in the message - dropping', 'error');
948 die;
949 }
950
951 $message_sequence_id = (int) $udrpc_message['sequence_id'];
952 $recently_seen_sequences_ids = $wpdb->get_var($wpdb->prepare('SELECT %s FROM %s LIMIT 1 WHERE '.$this->sequence_protection_where_sql, $this->sequence_protection_column, $this->sequence_protection_table));
953
954 if ('' === $recently_seen_sequences_ids) $recently_seen_sequences_ids = '0';
955
956 $recently_seen_sequences_ids_as_array = explode($recently_seen_sequences_ids, ',');
957 sort($recently_seen_sequences_ids_as_array);
958
959 // Seen before?
960 if (in_array($message_sequence_id, $recently_seen_sequences_ids_as_array)) {
961 $this->log("message with duplicate sequence number received - dropping (received=$message_sequence_id, seen=$recently_seen_sequences_ids)");
962 die;
963 }
964
965 // Within the tolerance threshold? That means: a) either bigger than the max, or b) no more than <tolerance> lower than the least
966 if ($message_sequence_id > max($recently_seen_sequences_ids)) {
967 if ($this->debug) $this->log("Sequence id ($message_sequence_id) is greater than any previous (".max($recently_seen_sequences_ids).') - message is thus OK');
968 // All is well
969 $recently_seen_sequences_ids_as_array[] = $message_sequence_id;
970 } elseif ((max($recently_seen_sequences_ids) - $message_sequence_id) <= $this->sequence_protection_tolerance) {
971 // All is well - was one of those 'missing' in the sequence
972 if ($this->debug) $this->log("Sequence id ($message_sequence_id) is within tolerance range of previous maximum (".max($recently_seen_sequences_ids).') - message is thus OK');
973 $recently_seen_sequences_ids_as_array[] = $message_sequence_id;
974 } else {
975 $this->log("message received outside of allowed sequence window - dropping (received=$message_sequence_id, seen=$recently_seen_sequences_ids, tolerance=".$this->sequence_protection_tolerance.')', 'error');
976 die;
977 }
978
979 // Remove out-of-bounds seen IDs
980 $max_sequence_id_seen = max($recently_seen_sequences_ids_as_array);
981 foreach ($recently_seen_sequences_ids_as_array as $k => $id) {
982 if ($max_sequence_id_seen - $id > $this->sequence_protection_tolerance) {
983 if ($this->debug) $this->log("Removing no-longer-relevant sequence from list of those recently seen: $id");
984 unset($recently_seen_sequences_ids_as_array[$k]);
985 }
986 }
987
988 // Allow reset
989 if ($current_sequence_id > PHP_INT_MAX - 10) {
990 $recently_seen_sequences_ids_as_array = array(0);
991 }
992
993 // Write them back to the database
994 $sql = $wpdb->prepare('UPDATE %s SET %s=%s WHERE '.$this->sequence_protection_where_sql, $this->sequence_protection_table, $this->sequence_protection_column, implode(',', $recently_seen_sequences_ids_as_array));
995 if ($this->debug) $this->log("SQL to send recent sequence IDs back to the database: $sql");
996 $wpdb->query($sql);
997
998 }
999
1000 $this->incoming_message = $udrpc_message;
1001
1002 $command = (string) $udrpc_message['command'];
1003 $data = empty($udrpc_message['data']) ? null : $udrpc_message['data'];
1004
1005 // @codingStandardsIgnoreLine
1006 if ($http_origin && !empty($udrpc_message['cors_headers_wanted']) && !@constant('UDRPC_DO_NOT_SEND_CORS_HEADERS')) {
1007 header("Access-Control-Allow-Origin: $http_origin");
1008 header('Access-Control-Allow-Credentials: true');
1009 }
1010
1011 $this->log('Command received: '.$command, 'info');
1012
1013 if ('ping' == $command) {
1014 echo json_encode($this->create_message('pong', null, true));
1015 } else {
1016 if (has_filter('udrpc_command_'.$command)) {
1017 $command_action_hooked = true;
1018 $response = apply_filters('udrpc_command_'.$command, null, $data, $this->key_name_indicator);
1019 } else {
1020 $response = array('response' => 'rpcerror', 'data' => array('code' => 'unknown_rpc_command', 'data' => $command));
1021 }
1022
1023 $response = apply_filters('udrpc_action', $response, $command, $data, $this->key_name_indicator, $this);
1024
1025 if (is_array($response)) {
1026
1027 if ($this->debug) {
1028 $this->log('UDRPC response (pre-encoding/encryption): '.serialize($response));
1029 }
1030
1031 $data = isset($response['data']) ? $response['data'] : null;
1032 echo json_encode($this->create_message($response['response'], $data, true));
1033 }
1034
1035 }
1036
1037 die;
1038
1039 }
1040
1041 /**
1042 * The hash needs to be in a format that phpseclib likes. phpseclib uses lower case.
1043 * Pass in a base64-encoded signature (i.e. just as signature_for_message creates)
1044 *
1045 * @param string $message
1046 * @param string $signature
1047 * @param string $key
1048 * @param string $hash_algorithm
1049 * @return boolean
1050 */
1051 public function verify_signature($message, $signature, $key, $hash_algorithm = 'sha256') {
1052 $this->ensure_crypto_loaded();
1053 $rsa = new Crypt_RSA();
1054 $rsa->setHash(strtolower($hash_algorithm));
1055 // This is not the default, but is what we use
1056 $rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
1057 $rsa->loadKey($key);
1058
1059 // Don't hash it - Crypt_RSA::verify() already does that
1060 // $hash = new Crypt_Hash($hash_algorithm);
1061 // $hashed = $hash->hash($message);
1062
1063 $verified = $rsa->verify($message, base64_decode($signature));
1064
1065 if ($this->debug) $this->log('Signature verification result: '.serialize($verified));
1066
1067 return $verified;
1068 }
1069
1070 private function calculate_message_hash($message) {
1071 return hash('sha256', $message);
1072 }
1073
1074 private function message_hash_seen($message_hash) {
1075 // 39 characters - less than the WP site transient name limit (40). Though, we use a normal transient, as these don't auto-load at all times.
1076 $transient_name = 'udrpch_'.md5($this->key_name_indicator);
1077 $seen_hashes = get_transient($transient_name);
1078 if (!is_array($seen_hashes)) $seen_hashes = array();
1079 $time_now = time();
1080 // $any_changes = false;
1081 // Prune the old hashes
1082 foreach ($seen_hashes as $hash => $last_seen) {
1083 if ($last_seen < ($time_now - $this->maximum_replay_time_difference)) {
1084 // $any_changes = true;
1085 unset($seen_hashes[$hash]);
1086 }
1087 }
1088 if (isset($seen_hashes[$message_hash])) {
1089 return true;
1090 }
1091 $seen_hashes[$message_hash] = $time_now;
1092 set_transient($transient_name, $seen_hashes, $this->maximum_replay_time_difference);
1093
1094 return false;
1095 }
1096 }
1097
1098 endif;
1099