PluginProbe ʕ •ᴥ•ʔ
UpdraftPlus: WP Backup & Migration Plugin / 1.12.40
UpdraftPlus: WP Backup & Migration Plugin v1.12.40
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
Dropbox 9 years ago Dropbox2 9 years ago Google 9 years ago cloudfiles 12 years ago images 9 years ago labelauty 9 years ago phpseclib 9 years ago S3.php 9 years ago S3compat.php 9 years ago cacert.pem 9 years ago class-commands.php 9 years ago class-database-utility.php 9 years ago class-partialfileservlet.php 9 years ago class-semaphore.php 9 years ago class-udrpc.php 9 years ago class-updraftcentral-updraftplus-commands.php 9 years ago class-wpadmin-commands.php 9 years ago deprecated-actions.php 9 years ago ftp.class.php 9 years ago get-cpanel-quota-usage.pl 12 years ago google-extensions.php 9 years ago jquery-ui.custom.css 9 years ago jquery.blockUI.js 9 years ago updraft-admin.js 9 years ago updraft-notices.php 9 years ago updraftplus-notices.php 9 years ago
class-udrpc.php
984 lines
1 <?php
2
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
58 if (!class_exists('UpdraftPlus_Remote_Communications')):
59 class UpdraftPlus_Remote_Communications {
60 // 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)
61 public $version = '1.4.12';
62
63 private $key_name_indicator;
64
65 private $key_option_name = false;
66 private $key_remote = false;
67 private $key_local = false;
68
69 private $can_generate = false;
70
71 private $destination_url = false;
72
73 private $maximum_replay_time_difference = 300;
74 private $extra_replay_protection = false;
75
76 private $sequence_protection_tolerance;
77 private $sequence_protection_table;
78 private $sequence_protection_column;
79 private $sequence_protection_where_sql;
80
81 // Debug may log confidential data using $this->log() - so only use when you are in a secure environment
82 private $debug = false;
83
84 private $next_send_sequence_id;
85
86 private $allow_cors_from = array();
87
88 private $http_transport = null;
89
90 // Default protocol version - this can be over-ridden with set_message_format
91 // Protocol version 1 (which uses only one RSA key-pair, instead of two) is legacy/deprecated
92 private $format = 2;
93
94 private $http_credentials = array();
95
96 private $incoming_message = null;
97
98 private $message_random_number = null;
99
100 public function __construct($key_name_indicator = 'default', $can_generate = false) {
101 $this->set_key_name_indicator($key_name_indicator);
102 }
103
104 public function set_key_name_indicator($key_name_indicator) {
105 $this->key_name_indicator = $key_name_indicator;
106 }
107
108 public function set_can_generate($can_generate = true) {
109 $this->can_generate = $can_generate;
110 }
111
112 // Which sites to allow CORS requests from
113 public function set_allow_cors_from($allow_cors_from) {
114 $this->allow_cors_from = $allow_cors_from;
115 }
116
117 public function set_maximum_replay_time_difference($replay_time_difference) {
118 $this->maximum_replay_time_difference = (int) $replay_time_difference;
119 }
120
121 // This will cause more things to be sent to $this->log()
122 public function set_debug($debug = true) {
123 $this->debug = (bool) $debug;
124 }
125
126 // Supported values: a Guzzle object, or, if not, then WP's HTTP API function siwll be used
127 public function set_http_transport($transport) {
128 $this->http_transport = $transport;
129 }
130
131 // 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).
132 // 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).
133 // The given table/column will record a comma-separated list of recently seen sequences numbers within the tolerance threshold.
134 public function activate_sequence_protection($table, $column, $where_sql, $tolerance = 5) {
135 $this->sequence_protection_tolerance = (int) $tolerance;
136 $this->sequence_protection_table = (string) $table;
137 $this->sequence_protection_column = (string) $column;
138 $this->sequence_protection_where_sql = (string) $where_sql;
139 }
140
141 private function ensure_crypto_loaded() {
142 if (!class_exists('Crypt_Rijndael') || !class_exists('Crypt_RSA') || !class_exists('Crypt_Hash')) {
143 global $updraftplus;
144 // phpseclib 1.x uses deprecated PHP4-style constructors
145 $this->no_deprecation_warnings_on_php7();
146 if (is_a($updraftplus, 'UpdraftPlus')) {
147 $updraftplus->ensure_phpseclib(array('Crypt_Rijndael', 'Crypt_RSA', 'Crypt_Hash'), array('Crypt/Rijndael', 'Crypt/RSA', 'Crypt/Hash'));
148 } elseif (defined('UPDRAFTPLUS_DIR') && file_exists(UPDRAFTPLUS_DIR.'/includes/phpseclib')) {
149 if (false === strpos(get_include_path(), UPDRAFTPLUS_DIR.'/includes/phpseclib')) set_include_path(UPDRAFTPLUS_DIR.'/includes/phpseclib'.PATH_SEPARATOR.get_include_path());
150 if (!class_exists('Crypt_Rijndael')) require_once 'Crypt/Rijndael.php';
151 if (!class_exists('Crypt_RSA')) require_once 'Crypt/RSA.php';
152 if (!class_exists('Crypt_Hash')) require_once 'Crypt/Hash.php';
153 } elseif (file_exists(dirname(dirname(__FILE__)).'/vendor/phpseclib/phpseclib/phpseclib')) {
154 $pdir = dirname(dirname(__FILE__)).'/vendor/phpseclib/phpseclib/phpseclib';
155 if (false === strpos(get_include_path(), $pdir)) set_include_path($pdir.PATH_SEPARATOR.get_include_path());
156 if (!class_exists('Crypt_Rijndael')) require_once 'Crypt/Rijndael.php';
157 if (!class_exists('Crypt_RSA')) require_once 'Crypt/RSA.php';
158 if (!class_exists('Crypt_Hash')) require_once 'Crypt/Hash.php';
159 }
160 }
161 }
162
163 // Ugly, but necessary to prevent debug output breaking the conversation when the user has debug turned on
164 private function no_deprecation_warnings_on_php7() {
165 // PHP_MAJOR_VERSION is defined in PHP 5.2.7+
166 // 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).
167 if (defined('PHP_MAJOR_VERSION') && PHP_MAJOR_VERSION == 7) {
168 $old_level = error_reporting();
169 $new_level = $old_level & ~E_DEPRECATED;
170 if ($old_level != $new_level) error_reporting($new_level);
171 }
172 }
173
174 public function set_destination_url($destination_url) {
175 $this->destination_url = $destination_url;
176 }
177
178 public function get_destination_url() {
179 return $this->destination_url;
180 }
181
182 public function set_option_name($key_option_name) {
183 $this->key_option_name = $key_option_name;
184 }
185
186 // Method to get the remote key
187 public function get_key_remote() {
188 if (empty($this->key_remote) && $this->can_generate) {
189 $this->generate_new_keypair();
190 }
191
192 return empty($this->key_remote) ? false : $this->key_remote;
193 }
194
195 // Set the remote key
196 public function set_key_remote($key_remote) {
197 $this->key_remote = $key_remote;
198 }
199
200 // Used for sending - when receiving, the format is part of the message
201 public function set_message_format($format = 2) {
202 $this->format = $format;
203 }
204
205 // Method to get the local key
206 public function get_key_local() {
207 if (empty($this->key_local)) {
208 if ($this->key_option_name) {
209 $key_local = get_site_option($this->key_option_name);
210 if ($key_local) {
211 $this->key_local = $key_local;
212 }
213 }
214 }
215 if (empty($this->key_local) && $this->can_generate) {
216 $this->generate_new_keypair();
217 }
218
219 return empty($this->key_local) ? false : $this->key_local;
220 }
221
222 // Tests whether a supplied string (after trimming) is a valid portable bundle
223 // Valid formats: same as get_portable_bundle()
224 // Returns: (array)an array (which the consumer is free to use - e.g. convert into internationalised string), with keys 'code' and (perhaps) 'data'
225 // Error codes: 'invalid_wrong_length'|'invalid_corrupt'
226 // Success codes: 'success' - then has further keys 'key', 'name_indicator' and 'url' (and anything else that was in the bundle)
227 public function decode_portable_bundle($bundle, $format = 'raw') {
228 $bundle = trim($bundle);
229 if ('base64_with_count' == $format) {
230 if (strlen($bundle) < 5) return array('code' => 'invalid_wrong_length', 'data' => 'too_short');
231 $len = substr($bundle, 0, 4);
232 $bundle = substr($bundle, 4);
233 $len = hexdec($len);
234 if (strlen($bundle) != $len) return array('code' => 'invalid_wrong_length', 'data' => "1,$len,".strlen($bundle));
235 if (false === ($bundle = base64_decode($bundle))) return array('code' => 'invalid_corrupt', 'data' => 'not_base64');
236 if (null === ($bundle = json_decode($bundle, true))) return array('code' => 'invalid_corrupt', 'data' => 'not_json');
237 }
238 if (empty($bundle['key'])) return array('code' => 'invalid_corrupt', 'data' => 'no_key');
239 if (empty($bundle['url'])) return array('code' => 'invalid_corrupt', 'data' => 'no_url');
240 if (empty($bundle['name_indicator'])) return array('code' => 'invalid_corrupt', 'data' => 'no_name_indicator');
241
242 return $bundle;
243 }
244
245 // 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)
246 // Supported formats: base64_with_count | (default)raw
247 // $extra_info needs to be JSON-serialisable, so be careful about what you put into it.
248 public function get_portable_bundle($format = 'raw', $extra_info = array(), $options = array()) {
249
250 $bundle = array_merge($extra_info, array(
251 'key' => empty($options['key']) ? $this->get_key_remote() : $options['key'],
252 'name_indicator' => $this->key_name_indicator,
253 'url' => trailingslashit(network_site_url()),
254 'admin_url' => trailingslashit(admin_url()),
255 'network_admin_url' => trailingslashit(network_admin_url()),
256 ));
257
258 if ('base64_with_count' == $format) {
259 $bundle = base64_encode(json_encode($bundle));
260
261 $len = strlen($bundle); // Get the length
262 $len = dechex($len); // The first bytes of the message are the bundle length
263 $len = str_pad($len, 4, '0', STR_PAD_LEFT); // Zero pad
264
265 return $len.$bundle;
266
267 } else {
268 return $bundle;
269 }
270
271 }
272
273 public function set_key_local($key_local) {
274 $this->key_local = $key_local;
275 if ($this->key_option_name) update_site_option($this->key_option_name, $this->key_local);
276 }
277
278 public function generate_new_keypair($key_size = 2048) {
279
280 $this->ensure_crypto_loaded();
281
282 $rsa = new Crypt_RSA();
283 $keys = $rsa->createKey($key_size);
284
285 if (empty($keys['privatekey'])) {
286 $this->set_key_local(false);
287 } else {
288 $this->set_key_local($keys['privatekey']);
289 }
290
291 if (empty($keys['publickey'])) {
292 $this->set_key_remote(false);
293 } else {
294 $this->set_key_remote($keys['publickey']);
295 }
296
297 return empty($keys['publickey']) ? false : true;
298 }
299
300 // A base-64 encoded RSA hash (PKCS_1) of the message digest
301 public function signature_for_message($message, $use_key = false) {
302
303 $hash_algorithm = 'sha256';
304
305 // Sign with the private (local) key
306 if (!$use_key) {
307 if (!$this->key_local) throw new Exception('No signing key has been set');
308 $use_key = $this->key_local;
309 }
310
311 $this->ensure_crypto_loaded();
312
313 $rsa = new Crypt_RSA();
314 $rsa->loadKey($use_key);
315 // 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
316 $rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
317
318 // Don't do this: Crypt_RSA::sign() already calculates the digest of the hash
319 // $hash = new Crypt_Hash($hash_algorithm);
320 // $hashed = $hash->hash($message);
321
322 // if ($this->debug) $this->log("Message hash (hash=$hash_algorithm) (hex): ".bin2hex($hashed));
323
324 // phpseclib defaults to SHA1
325 $rsa->setHash($hash_algorithm);
326 $encrypted = $rsa->sign($message);
327
328 if ($this->debug) $this->log('Signed hash (mode='.CRYPT_RSA_SIGNATURE_PKCS1.') (hex): '.bin2hex($encrypted));
329
330 $signature = base64_encode($encrypted);
331
332 if ($this->debug) $this->log("Message signature (base64): $signature");
333
334 return $signature;
335 }
336
337 // $level is not yet used much
338 private function log($message, $level = 'notice') {
339 // Allow other plugins to do something with the message
340 do_action('udrpc_log', $message, $level, $this->key_name_indicator, $this->debug, $this);
341 if ($level != 'info') error_log('UDRPC ('.$this->key_name_indicator.", $level): $message");
342 }
343
344 // Encrypt the message, using the local key (which needs to exist)
345 public function encrypt_message($plaintext, $use_key = false, $key_length = 32) {
346
347 if (!$use_key) {
348 if ($this->format == 1) {
349 if (!$this->key_local) throw new Exception('No encryption key has been set');
350 $use_key = $this->key_local;
351 } else {
352 if (!$this->key_remote) throw new Exception('No encryption key has been set');
353 $use_key = $this->key_remote;
354 }
355 }
356
357 $this->ensure_crypto_loaded();
358
359 $rsa = new Crypt_RSA();
360
361 if (defined('UDRPC_PHPSECLIB_ENCRYPTION_MODE')) $rsa->setEncryptionMode(UDRPC_PHPSECLIB_ENCRYPTION_MODE);
362
363 $rij = new Crypt_Rijndael();
364
365 // Generate Random Symmetric Key
366 $sym_key = crypt_random_string($key_length);
367
368 if ($this->debug) $this->log('Unencrypted symmetric key (hex): '.bin2hex($sym_key));
369
370 // Encrypt Message with new Symmetric Key
371 $rij->setKey($sym_key);
372 $ciphertext = $rij->encrypt($plaintext);
373
374 if ($this->debug) $this->log('Encrypted ciphertext (hex): '.bin2hex($ciphertext));
375
376 $ciphertext = base64_encode($ciphertext);
377
378 // Encrypt the Symmetric Key with the Asymmetric Key
379 $rsa->loadKey($use_key);
380 $sym_key = $rsa->encrypt($sym_key);
381
382 if ($this->debug) $this->log('Encrypted symmetric key (hex): '.bin2hex($sym_key));
383
384 // Base 64 encode the symmetric key for transport
385 $sym_key = base64_encode($sym_key);
386
387 if ($this->debug) $this->log('Encrypted symmetric key (b64): '.$sym_key);
388
389 $len = str_pad(dechex(strlen($sym_key)), 3, '0', STR_PAD_LEFT); // Zero pad to be sure.
390
391 // 16 characters of hex is enough for the payload to be to 16 exabytes (giga < tera < peta < exa) of data
392 $cipherlen = str_pad(dechex(strlen($ciphertext)), 16, '0', STR_PAD_LEFT);
393
394 // Concatenate the length, the encrypted symmetric key, and the message
395 return $len.$sym_key.$cipherlen.$ciphertext;
396
397 }
398
399 // Decrypt the message, using the local key (which needs to exist)
400 public function decrypt_message($message) {
401
402 if (!$this->key_local) throw new Exception('No decryption key has been set');
403
404 $this->ensure_crypto_loaded();
405
406 $rsa = new Crypt_RSA();
407 if (defined('UDRPC_PHPSECLIB_ENCRYPTION_MODE')) $rsa->setEncryptionMode(UDRPC_PHPSECLIB_ENCRYPTION_MODE);
408 // Defaults to CRYPT_AES_MODE_CBC
409 $rij = new Crypt_Rijndael();
410
411 // Extract the Symmetric Key
412 $len = substr($message, 0, 3);
413 $len = hexdec($len);
414 $sym_key = substr($message, 3, $len);
415
416 // Extract the encrypted message
417 $cipherlen = substr($message, $len + 3, 16);
418 $cipherlen = hexdec($cipherlen);
419
420 $ciphertext = substr($message, $len + 19, $cipherlen);
421 $ciphertext = base64_decode($ciphertext);
422
423 // Decrypt the encrypted symmetric key
424 $rsa->loadKey($this->key_local);
425 $sym_key = base64_decode($sym_key);
426 $sym_key = $rsa->decrypt($sym_key);
427
428 // Decrypt the message
429 $rij->setKey($sym_key);
430
431 return $rij->decrypt($ciphertext);
432
433 }
434
435 // Returns an array - which the caller will then format as required (e.g. use as body in post, or JSON-encode, etc.)
436 public function create_message($command, $data = null, $is_response = false, $use_key_remote = false, $use_key_local = false) {
437
438 if ($is_response) {
439 $send_array = array('response' => $command);
440 } else {
441 $send_array = array('command' => $command);
442 }
443
444 $send_array['time'] = time();
445 // This goes in the encrypted portion as well to prevent replays with a different unencrypted name indicator
446 $send_array['key_name'] = $this->key_name_indicator;
447
448 // 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
449 // The value of PHP_INT_MAX on a 32-bit platform
450 $this->message_random_number = rand(1, 2147483647);
451 $send_array['rand'] = $this->message_random_number;
452
453 if ($this->next_send_sequence_id) {
454 $send_array['sequence_id'] = $this->next_send_sequence_id;
455 ++$this->next_send_sequence_id;
456 }
457
458 if ($is_response && !empty($this->incoming_message) && isset($this->incoming_message['rand'])) {
459 $send_array['incoming_rand'] = $this->incoming_message['rand'];
460 }
461
462 if (null !== $data) $send_array['data'] = $data;
463 $send_data = $this->encrypt_message(json_encode($send_array), $use_key_remote);
464
465 $message = array(
466 'format' => $this->format,
467 'key_name' => $this->key_name_indicator,
468 'udrpc_message' => $send_data,
469 );
470
471 if ($this->format >= 2) {
472 $signature = $this->signature_for_message($send_data, $use_key_local);
473 $message['signature'] = $signature;
474 }
475
476 return $message;
477
478 }
479
480 // N.B. There's already some time-based replay protection. This can be turned on to beef it up.
481 // 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).
482 public function activate_replay_protection($activate = true) {
483 $this->extra_replay_protection = (bool) $activate;
484 }
485
486 public function set_next_send_sequence_id($id) {
487 $this->next_send_sequence_id = $id;
488 }
489
490 // $credentials should be an array with entries for 'username' and 'password'
491 public function set_http_credentials($credentials) {
492 $this->http_credentials = $credentials;
493 }
494
495 // 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)
496 // The $post_options array support these keys: timeout, body,
497 // 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)
498 public function http_post($post_options) {
499
500 @include ABSPATH.WPINC.'/version.php';
501 $http_credentials = $this->http_credentials;
502
503 if (is_a($this->http_transport, 'GuzzleHttp\Client')) {
504
505 // https://guzzle.readthedocs.org/en/5.3/clients.html
506
507 $client = $this->http_transport;
508
509 $guzzle_options = array(
510 'body' => $post_options['body'],
511 'headers' => array(
512 'User-Agent' => 'WordPress/'.$wp_version.'; class-udrpc.php-Guzzle/'.$this->version.'; '.get_bloginfo('url'),
513 ),
514 'exceptions' => false,
515 'timeout' => $post_options['timeout'],
516 );
517
518 if (!class_exists('WP_HTTP_Proxy')) require_once ABSPATH.WPINC.'/class-http.php';
519 $proxy = new WP_HTTP_Proxy();
520 if ($proxy->is_enabled()) {
521 $user = $proxy->username();
522 $pass = $proxy->password();
523 $host = $proxy->host();
524 $port = (int) $proxy->port();
525 if (empty($port)) $port = 8080;
526 if (!empty($host) && $proxy->send_through_proxy($this->destination_url)) {
527 $proxy_auth = '';
528 if (!empty($user)) {
529 $proxy_auth = $user;
530 if (!empty($pass)) $proxy_auth .= ':'.$pass;
531 $proxy_auth .= '@';
532 }
533 $guzzle_options['proxy'] = array(
534 'http' => "http://${proxy_auth}$host:$port",
535 'https' => "http://${proxy_auth}$host:$port",
536 );
537 }
538 }
539
540 if (defined('UDRPC_GUZZLE_SSL_VERIFY')) {
541 $verify = UDRPC_GUZZLE_SSL_VERIFY;
542 } elseif (file_exists(ABSPATH.WPINC.'/certificates/ca-bundle.crt')) {
543 $verify = ABSPATH.WPINC.'/certificates/ca-bundle.crt';
544 } else {
545 $verify = true;
546 }
547 $guzzle_options['verify'] = apply_filters('udrpc_guzzle_verify', $verify);
548
549 if (!empty($http_credentials['username'])) {
550
551 $authentication_method = empty($http_credentials['authentication_method']) ? 'basic' : $http_credentials['authentication_method'];
552
553 $password = empty($http_credentials['password']) ? '' : $http_credentials['password'];
554
555 $guzzle_options['auth'] = array(
556 $http_credentials['username'],
557 $password,
558 $authentication_method,
559 );
560
561 }
562
563 $response = $client->post($this->destination_url, apply_filters('udrpc_guzzle_options', $guzzle_options, $this));
564
565 $formatted_response = array(
566 'response' => array(
567 'code' => $response->getStatusCode(),
568 ),
569 'body' => $response->getBody(),
570 );
571
572 return $formatted_response;
573
574 } else {
575
576 $post_options['user-agent'] = 'WordPress/'.$wp_version.'; class-udrpc.php/'.$this->version.'; '.get_bloginfo('url');
577
578 if (!empty($http_credentials['username'])) {
579
580 $authentication_type = empty($http_credentials['authentication_type']) ? 'basic' : $http_credentials['authentication_type'];
581
582 if ('basic' != $authentication_type) {
583 return new WP_Error('unsupported_http_authentication_type', 'Only HTTP basic authentication is supported (for other types, use Guzzle)');
584 }
585
586 $password = empty($http_credentials['password']) ? '' : $http_credentials['password'];
587 $post_options['headers'] = array(
588 'Authorization' => 'Basic '.base64_encode($http_credentials['username'].':'.$password),
589 );
590 }
591
592 return wp_remote_post(
593 $this->destination_url,
594 $post_options
595 );
596 }
597 }
598
599 public function send_message($command, $data = null, $timeout = 20) {
600
601 if (empty($this->destination_url)) return new WP_Error('not_initialised', 'RPC error: URL not initialised');
602
603 $message = $this->create_message($command, $data);
604
605 $post_options = array(
606 'timeout' => $timeout,
607 'body' => $message,
608 );
609
610 $post_options = apply_filters('udrpc_post_options', $post_options, $command, $data, $timeout, $this);
611
612 try {
613 $post = $this->http_post($post_options);
614 } catch (Exception $e) {
615 // Curl can return an error code 0, which causes WP_Error to return early, without recording the message. So, we prefix the code.
616 return new WP_Error('http_post_'.$e->getCode(), $e->getMessage());
617 }
618
619 if (is_wp_error($post)) return $post;
620
621 $response_code = wp_remote_retrieve_response_code($post);
622
623 if (empty($response_code)) return new WP_Error('empty_http_code', 'Unexpected HTTP response code');
624
625 if ($response_code < 200 || $response_code >= 300) return new WP_Error('unexpected_http_code', 'Unexpected HTTP response code ('.$response_code.')', $post);
626
627 $response_body = wp_remote_retrieve_body($post);
628
629 if (empty($response_body)) return new WP_Error('empty_response', 'Empty response from remote site');
630
631 $decoded = json_decode($response_body, true);
632
633 if (empty($decoded)) {
634
635 if (false != ($found_at = strpos($response_body, '{"format":'))) {
636 $new_body = substr($response_body, $found_at);
637 $decoded = json_decode($new_body, true);
638 }
639
640 if (empty($decoded)) {
641 $this->log('response from remote site could not be understood: '.substr($response_body, 0, 100).' ... ');
642
643 return new WP_Error('response_not_understood', 'Response from remote site could not be understood', $response_body);
644 }
645 }
646
647 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);
648
649 if ($this->format >= 2) {
650 if (empty($decoded['signature'])) {
651 $this->log('No message signature found');
652 die;
653 }
654 if (!$this->key_remote) {
655 $this->log('No signature verification key has been set');
656 die;
657 }
658 if (!$this->verify_signature($decoded['udrpc_message'], $decoded['signature'], $this->key_remote)) {
659 $this->log('Signature verification failed; discarding');
660 die;
661 }
662 }
663
664 $decoded = $this->decrypt_message($decoded['udrpc_message']);
665
666 if (!is_string($decoded)) return new WP_Error('not_decrypted', 'Response from remote site was not successfully decrypted', $decoded['udrpc_message']);
667
668 $json_decoded = json_decode($decoded, true);
669
670 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);
671
672 // 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)
673 if ($this->extra_replay_protection) {
674 $message_hash = $this->calculate_message_hash((string) $post['body']);
675 if ($this->message_hash_seen($message_hash)) {
676 return new WP_Error('replay_detected', 'Message refused: replay detected', $message_hash);
677 }
678 }
679
680 $time_difference = absint(time() - $json_decoded['time']);
681 if ($time_difference > $this->maximum_replay_time_difference) return new WP_Error('window_error', 'Message refused: maxium replay time difference exceeded', $time_difference);
682
683 if (isset($json_decoded['incoming_rand']) && !empty($this->message_random_number) && $json_decoded['incoming_rand'] != $this->message_random_number) {
684 $this->log('UDRPC: Message mismatch (possibly MITM) (sent_rand=' + $this->message_random_number + ', returned_rand='.$json_decoded['incoming_rand'].'): dropping', 'error');
685
686 return new WP_Error('message_mismatch_error', 'Message refused: message mismatch (possible MITM)');
687
688 }
689
690 // Should be an array with keys including 'response' and (if relevant) 'data'
691 return $json_decoded;
692
693 }
694
695 // 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)
696 public function create_listener() {
697
698 $http_origin = function_exists('get_http_origin') ? get_http_origin() : (empty($_SERVER['HTTP_ORIGIN']) ? '' : $_SERVER['HTTP_ORIGIN']);
699
700 // 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
701 if ((!empty($_POST) && !empty($_POST['udrpc_message']) && !empty($_POST['format'])) || (!empty($_SERVER['REQUEST_METHOD']) && 'OPTIONS' == $_SERVER['REQUEST_METHOD'] && $http_origin)) {
702 add_action('wp_loaded', array($this, 'wp_loaded'));
703 add_action('wp_loaded', array($this, 'wp_loaded_final'), 10000);
704
705 return true;
706 }
707
708 return false;
709 }
710
711 public function wp_loaded_final() {
712 $message_for = empty($_POST['key_name']) ? '' : (string) $_POST['key_name'];
713 $this->log("Message was received, but not understood by local site (for: $message_for)");
714 die;
715 }
716
717 public function wp_loaded() {
718
719 /*
720 // What if something else already set some response headers?
721 if (function_exists('apache_response_headers')) {
722 $apache_response_headers = apache_response_headers();
723 // Do something...
724 }
725 */
726
727 // CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
728 // get_http_origin() : since WP 3.4
729 $http_origin = function_exists('get_http_origin') ? get_http_origin() : (empty($_SERVER['HTTP_ORIGIN']) ? '' : $_SERVER['HTTP_ORIGIN']);
730 if (!empty($_SERVER['REQUEST_METHOD']) && 'OPTIONS' == $_SERVER['REQUEST_METHOD'] && $http_origin) {
731 if (in_array($http_origin, $this->allow_cors_from)) {
732 if (!@constant('UDRPC_DO_NOT_SEND_CORS_HEADERS')) {
733 header("Access-Control-Allow-Origin: $http_origin");
734 header('Access-Control-Allow-Credentials: true');
735 if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) header('Access-Control-Allow-Methods: POST, OPTIONS');
736 if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) header('Access-Control-Allow-Headers: '.$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']);
737 }
738 die;
739 } elseif ($this->debug) {
740 $this->log('Non-allowed CORS from: '.$http_origin);
741 }
742 // 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.
743 return;
744 }
745
746 // Silently return, rather than dying, in case another instance is able to handle this
747 if (empty($_POST['format']) || (1 != $_POST['format'] && 2 != $_POST['format'])) return;
748
749 $format = $_POST['format'];
750
751 /*
752
753 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
754
755 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).
756
757 */
758
759 // Is this for us?
760 if (empty($_POST['key_name']) || $_POST['key_name'] != $this->key_name_indicator) {
761 return;
762 }
763
764 // wp_unslash() does not exist until after WP 3.5
765 // $udrpc_message = function_exists('wp_unslash') ? wp_unslash($_POST['udrpc_message']) : stripslashes_deep($_POST['udrpc_message']);
766
767 // Data should not have any slashes - it is base64-encoded
768 $udrpc_message = (string) $_POST['udrpc_message'];
769
770 // Check this now, rather than allow the decrypt method to thrown an Exception
771
772 if (empty($this->key_local)) {
773 $this->log('no local key (format 1): cannot decrypt', 'error');
774 die;
775 }
776
777 if ($format >= 2) {
778 if (empty($_POST['signature'])) {
779 $this->log('No message signature found', 'error');
780 die;
781 }
782 if (!$this->key_remote) {
783 $this->log('No signature verification key has been set', 'error');
784 die;
785 }
786 if (!$this->verify_signature($udrpc_message, $_POST['signature'], $this->key_remote)) {
787 $this->log('Signature verification failed; discarding', 'error');
788 die;
789 }
790 }
791
792 try {
793 $udrpc_message = $this->decrypt_message($udrpc_message);
794 } catch (Exception $e) {
795 $this->log('Exception ('.get_class($e).'): '.$e->getMessage(), 'error');
796 die;
797 }
798
799 $udrpc_message = json_decode($udrpc_message, true);
800
801 if (empty($udrpc_message) || !is_array($udrpc_message) || empty($udrpc_message['command']) || !is_string($udrpc_message['command'])) {
802 $this->log('Could not decode JSON on incoming message', 'error');
803 die;
804 }
805
806 if (empty($udrpc_message['time'])) {
807 $this->log('No time set in incoming message', 'error');
808 die;
809 }
810
811 // Mismatch indicating a replay of the message with a different key name in the unencrypted portion?
812 if (empty($udrpc_message['key_name']) || $_POST['key_name'] != $udrpc_message['key_name']) {
813 $this->log('key_name mismatch between encrypted and unencrypted portions', 'error');
814 die;
815 }
816
817 if ($this->extra_replay_protection) {
818 $message_hash = $this->calculate_message_hash((string) $_POST['udrpc_message']);
819 if ($this->message_hash_seen($message_hash)) {
820 $this->log("Message dropped: apparently a replay (hash: $message_hash)", 'error');
821 die;
822 }
823 }
824
825 // 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.
826 $time_difference = absint($udrpc_message['time'] - time());
827 if ($time_difference > $this->maximum_replay_time_difference) {
828 $this->log("Time in incoming message is outside of allowed window ($time_difference > ".$this->maximum_replay_time_difference.')', 'error');
829 die;
830 }
831
832 // The sequence number should always be larger than any previously-sent sequence number
833 if ($this->sequence_protection_tolerance) {
834
835 if ($this->debug) $this->log('Sequence protection is active; tolerance: '.$this->sequence_protection_tolerance);
836
837 global $wpdb;
838
839 if (!isset($udrpc_message['sequence_id']) || !is_numeric($udrpc_message['sequence_id'])) {
840 $this->log('a numerical sequence number is required, but none was included in the message - dropping', 'error');
841 die;
842 }
843
844 $message_sequence_id = (int) $udrpc_message['sequence_id'];
845 $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));
846
847 if ('' === $recently_seen_sequences_ids) $recently_seen_sequences_ids = '0';
848
849 $recently_seen_sequences_ids_as_array = explode($recently_seen_sequences_ids, ',');
850 sort($recently_seen_sequences_ids_as_array);
851
852 // Seen before?
853 if (in_array($message_sequence_id, $recently_seen_sequences_ids_as_array)) {
854 $this->log("message with duplicate sequence number received - dropping (received=$message_sequence_id, seen=$recently_seen_sequences_ids)");
855 die;
856 }
857
858 // Within the tolerance threshold? That means: a) either bigger than the max, or b) no more than <tolerance> lower than the least
859 if ($message_sequence_id > max($recently_seen_sequences_ids)) {
860 if ($this->debug) $this->log("Sequence id ($message_sequence_id) is greater than any previous (".max($recently_seen_sequences_ids).') - message is thus OK');
861 // All is well
862 $recently_seen_sequences_ids_as_array[] = $message_sequence_id;
863 } elseif (max($recently_seen_sequences_ids) - $message_sequence_id <= $this->sequence_protection_tolerance) {
864 // All is well - was one of those 'missing' in the sequence
865 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');
866 $recently_seen_sequences_ids_as_array[] = $message_sequence_id;
867 } else {
868 $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');
869 die;
870 }
871
872 // Remove out-of-bounds seen IDs
873 $max_sequence_id_seen = max($recently_seen_sequences_ids_as_array);
874 foreach ($recently_seen_sequences_ids_as_array as $k => $id) {
875 if ($max_sequence_id_seen - $id > $this->sequence_protection_tolerance) {
876 if ($this->debug) $this->log("Removing no-longer-relevant sequence from list of those recently seen: $id");
877 unset($recently_seen_sequences_ids_as_array[$k]);
878 }
879 }
880
881 // Allow reset
882 if ($current_sequence_id > PHP_INT_MAX - 10) {
883 $recently_seen_sequences_ids_as_array = array(0);
884 }
885
886 // Write them back to the database
887 $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));
888 if ($this->debug) $this->log("SQL to send recent sequence IDs back to the database: $sql");
889 $wpdb->query($sql);
890
891 }
892
893 $this->incoming_message = $udrpc_message;
894
895 $command = (string) $udrpc_message['command'];
896 $data = empty($udrpc_message['data']) ? null : $udrpc_message['data'];
897
898 if ($http_origin && !empty($udrpc_message['cors_headers_wanted']) && !@constant('UDRPC_DO_NOT_SEND_CORS_HEADERS')) {
899 header("Access-Control-Allow-Origin: $http_origin");
900 header('Access-Control-Allow-Credentials: true');
901 }
902
903 $this->log('Command received: '.$command, 'info');
904
905 if ('ping' == $command) {
906 echo json_encode($this->create_message('pong', null, true));
907 } else {
908 if (has_filter('udrpc_command_'.$command)) {
909 $command_action_hooked = true;
910 $response = apply_filters('udrpc_command_'.$command, null, $data, $this->key_name_indicator);
911 } else {
912 $response = array('response' => 'rpcerror', 'data' => array('code' => 'unknown_rpc_command', 'data' => $command));
913 }
914
915 $response = apply_filters('udrpc_action', $response, $command, $data, $this->key_name_indicator, $this);
916
917 if (is_array($response)) {
918
919 if ($this->debug) {
920 $this->log('UDRPC response (pre-encoding/encryption): '.serialize($response));
921 }
922
923 $data = isset($response['data']) ? $response['data'] : null;
924 echo json_encode($this->create_message($response['response'], $data, true));
925 }
926
927 }
928
929 die;
930
931 }
932
933 // The hash needs to be in a format that phpseclib likes. phpseclib uses lower case.
934 // Pass in a base64-encoded signature (i.e. just as signature_for_message creates)
935 // Returns a boolean
936 public function verify_signature($message, $signature, $key, $hash_algorithm = 'sha256') {
937 $this->ensure_crypto_loaded();
938 $rsa = new Crypt_RSA();
939 $rsa->setHash(strtolower($hash_algorithm));
940 // This is not the default, but is what we use
941 $rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
942 $rsa->loadKey($key);
943
944 // Don't hash it - Crypt_RSA::verify() already does that
945 // $hash = new Crypt_Hash($hash_algorithm);
946 // $hashed = $hash->hash($message);
947
948 $verified = $rsa->verify($message, base64_decode($signature));
949
950 if ($this->debug) $this->log('Signature verification result: '.serialize($verified));
951
952 return $verified;
953 }
954
955 private function calculate_message_hash($message) {
956 return hash('sha256', $message);
957 }
958
959 private function message_hash_seen($message_hash) {
960 // 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.
961 $transient_name = 'udrpch_'.md5($this->key_name_indicator);
962 $seen_hashes = get_transient($transient_name);
963 if (!is_array($seen_hashes)) $seen_hashes = array();
964 $time_now = time();
965 // $any_changes = false;
966 // Prune the old hashes
967 foreach ($seen_hashes as $hash => $last_seen) {
968 if ($last_seen < $time_now - $this->maximum_replay_time_difference) {
969 // $any_changes = true;
970 unset($seen_hashes[$hash]);
971 }
972 }
973 if (isset($seen_hashes[$message_hash])) {
974 return true;
975 }
976 $seen_hashes[$message_hash] = $time_now;
977 set_transient($transient_name, $seen_hashes, $this->maximum_replay_time_difference);
978
979 return false;
980 }
981
982 }
983 endif;
984