// @codingStandardsIgnoreStart
This class provides methods for encrypting, sending, receiving and decrypting messages of arbitrary length, using standard encryption methods and including protection against replay attacks.
// Set a key and encrypt with it
$ud_rpc = new UpdraftPlus_Remote_Communications($name_indicator); // $name_indicator is a key indicator - indicating which key is being used.
$ud_rpc->set_key_local($our_private_key);
$ud_rpc->set_key_remote($their_public_key);
$encrypted = $ud_rpc->encrypt_message('blah blah');
// Use the saved WP site option
$ud_rpc = new UpdraftPlus_Remote_Communications($name_indicator); // $name_indicator is a key indicator - indicating which key is being used.
$ud_rpc->set_option_name('udrpc_remotekey');
if (!$ud_rpc->get_key_remote()) throw new Exception('...');
$encrypted = $ud_rpc->encrypt_message('blah blah');
$ud_rpc = new UpdraftPlus_Remote_Communications('myindicator.example.com');
$ud_rpc->set_option_name('udrpc_localkey'); // Save as a WP site option
$new_pair = $ud_rpc->generate_new_keypair();
$local_private_key = $ud_rpc->get_key_local();
$remote_public_key = $ud_rpc->get_key_remote();
throw new Exception('...');
$ud_rpc->activate_replay_protection();
$ud_rpc->set_destination_url('https://example.com/path/to/wp');
$ud_rpc->send_message('ping');
$ud_rpc->send_message('somecommand', array('param1' => 'data', 'param2' => 'moredata'));
// N.B. The data sent needs to be something that will pass json_encode(). So, it may be desirable to base64-encode it first.
// Create a listener for incoming messages
add_filter('udrpc_command_somecommand', 'my_function', 10, 3);
// function my_function($response, $data, $name_indicator) { ... ; return array('response' => 'my_reply', 'data' => 'any mixed data'); }
// 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.
// function some_function($response, $command, $data, $name_indicator) { ...; return array('response' => 'my_reply', 'data' => 'any mixed data'); }
$ud_rpc->set_option_name('udrpc_local_private_key');
$ud_rpc->activate_replay_protection();
if ($ud_rpc->get_key_local()) {
// Make sure you call this before the wp_loaded action is fired (e.g. at init)
$ud_rpc->create_listener();
// 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).
// @codingStandardsIgnoreEnd
if (!class_exists('UpdraftPlus_Remote_Communications')) :
class UpdraftPlus_Remote_Communications {
// 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)
public $version = '1.4.23';
private $key_name_indicator;
private $key_option_name = false;
private $key_remote = false;
private $key_local = false;
private $can_generate = false;
private $destination_url = false;
private $maximum_replay_time_difference = 300;
private $extra_replay_protection = false;
private $sequence_protection_tolerance;
private $sequence_protection_table;
private $sequence_protection_column;
private $sequence_protection_where_sql;
// Debug may log confidential data using $this->log() - so only use when you are in a secure environment
private $next_send_sequence_id;
private $allow_cors_from = array();
private $http_transport = null;
// Default protocol version - this can be over-ridden with set_message_format
// Protocol version 1 (which uses only one RSA key-pair, instead of two) is legacy/deprecated
private $http_credentials = array();
private $incoming_message = null;
private $message_random_number = null;
private $require_message_to_be_understood = false;
public function __construct($key_name_indicator = 'default') {
$this->set_key_name_indicator($key_name_indicator);
public function set_key_name_indicator($key_name_indicator) {
$this->key_name_indicator = $key_name_indicator;
public function set_can_generate($can_generate = true) {
$this->can_generate = $can_generate;
* Which sites to allow CORS requests from
* @param string $allow_cors_from
public function set_allow_cors_from($allow_cors_from) {
$this->allow_cors_from = $allow_cors_from;
public function set_maximum_replay_time_difference($replay_time_difference) {
$this->maximum_replay_time_difference = (int) $replay_time_difference;
* This will cause more things to be sent to $this->log()
public function set_debug($debug = true) {
$this->debug = (bool) $debug;
* Supported values: a Guzzle object, or, if not, then WP's HTTP API function siwll be used
* @param string $transport
public function set_http_transport($transport) {
$this->http_transport = $transport;
* 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).
* 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).
* The given table/column will record a comma-separated list of recently seen sequences numbers within the tolerance threshold.
* @param string $where_sql
* @param integer $tolerance
public function activate_sequence_protection($table, $column, $where_sql, $tolerance = 5) {
$this->sequence_protection_tolerance = (int) $tolerance;
$this->sequence_protection_table = (string) $table;
$this->sequence_protection_column = (string) $column;
$this->sequence_protection_where_sql = (string) $where_sql;
private function ensure_crypto_loaded() {
if (!class_exists('Crypt_Rijndael') || !class_exists('Crypt_RSA') || !class_exists('Crypt_Hash')) {
// phpseclib 1.x uses deprecated PHP4-style constructors
$this->no_deprecation_warnings_on_php7();
if (is_a($updraftplus, 'UpdraftPlus')) {
// Since May 2019, the second parameter is unused; but, since we don't know the version, we send it.
$ensure_phpseclib = $updraftplus->ensure_phpseclib(array('Crypt_Rijndael', 'Crypt_RSA', 'Crypt_Hash'), array('Crypt/Rijndael', 'Crypt/RSA', 'Crypt/Hash'));
if (is_wp_error($ensure_phpseclib)) return $ensure_phpseclib;
} elseif (defined('UPDRAFTPLUS_DIR') && file_exists(UPDRAFTPLUS_DIR.'/vendor/phpseclib/phpseclib/phpseclib')) {
$pdir = UPDRAFTPLUS_DIR.'/vendor/phpseclib/phpseclib/phpseclib';
if (false === strpos(get_include_path(), $pdir)) set_include_path($pdir.PATH_SEPARATOR.get_include_path());
if (!class_exists('Crypt_Rijndael')) include_once 'Crypt/Rijndael.php';
if (!class_exists('Crypt_RSA')) include_once 'Crypt/RSA.php';
if (!class_exists('Crypt_Hash')) include_once 'Crypt/Hash.php';
} elseif (file_exists(dirname(dirname(__FILE__)).'/vendor/phpseclib/phpseclib/phpseclib')) {
$pdir = dirname(dirname(__FILE__)).'/vendor/phpseclib/phpseclib/phpseclib';
if (false === strpos(get_include_path(), $pdir)) set_include_path($pdir.PATH_SEPARATOR.get_include_path());
if (!class_exists('Crypt_Rijndael')) include_once 'Crypt/Rijndael.php';
if (!class_exists('Crypt_RSA')) include_once 'Crypt/RSA.php';
if (!class_exists('Crypt_Hash')) include_once 'Crypt/Hash.php';
* Ugly, but necessary to prevent debug output breaking the conversation when the user has debug turned on
private function no_deprecation_warnings_on_php7() {
// PHP_MAJOR_VERSION is defined in PHP 5.2.7+
// 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).
// @codingStandardsIgnoreLine
if (defined('PHP_MAJOR_VERSION') && PHP_MAJOR_VERSION == 7) {
$old_level = error_reporting();
// @codingStandardsIgnoreLine
$new_level = $old_level & ~E_DEPRECATED;
if ($old_level != $new_level) error_reporting($new_level);
public function set_destination_url($destination_url) {
$this->destination_url = $destination_url;
public function get_destination_url() {
return $this->destination_url;
public function set_option_name($key_option_name) {
$this->key_option_name = $key_option_name;
* Method to get the remote key
public function get_key_remote() {
if (empty($this->key_remote) && $this->can_generate) {
$this->generate_new_keypair();
return empty($this->key_remote) ? false : $this->key_remote;
* @param string $key_remote
public function set_key_remote($key_remote) {
$this->key_remote = $key_remote;
* Used for sending - when receiving, the format is part of the message
public function set_message_format($format = 2) {
* Used for sending - when receiving, the format is part of the message
public function get_message_format() {
* Method to get the local key
public function get_key_local() {
if (empty($this->key_local)) {
if ($this->key_option_name) {
$key_local = get_site_option($this->key_option_name);
$this->key_local = $key_local;
if (empty($this->key_local) && $this->can_generate) {
$this->generate_new_keypair();
return empty($this->key_local) ? false : $this->key_local;
* Tests whether a supplied string (after trimming) is a valid portable bundle
* @param string $bundle [description]
* @param string $format same as get_portable_bundle()
* @return array (which the consumer is free to use - e.g. convert into internationalised string), with keys 'code' and (perhaps) 'data'
public function decode_portable_bundle($bundle, $format = 'raw') {
if ('base64_with_count' == $format) {
if (strlen($bundle) < 5) return array('code' => 'invalid_wrong_length', 'data' => 'too_short');
$len = substr($bundle, 0, 4);
$bundle = substr($bundle, 4);
if (strlen($bundle) != $len) return array('code' => 'invalid_wrong_length', 'data' => "1,$len,".strlen($bundle));
if (false === ($bundle = base64_decode($bundle))) return array('code' => 'invalid_corrupt', 'data' => 'not_base64');
if (null === ($bundle = json_decode($bundle, true))) return array('code' => 'invalid_corrupt', 'data' => 'not_json');
if (empty($bundle['key'])) return array('code' => 'invalid_corrupt', 'data' => 'no_key');
if (empty($bundle['url'])) return array('code' => 'invalid_corrupt', 'data' => 'no_url');
if (empty($bundle['name_indicator'])) return array('code' => 'invalid_corrupt', 'data' => 'no_name_indicator');
* 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)
* @param string $format Supported formats: base64_with_count and default)raw
* @param array $extra_info needs to be JSON-serialisable, so be careful about what you put into it.
* @param array $options [description]
public function get_portable_bundle($format = 'raw', $extra_info = array(), $options = array()) {
$bundle = array_merge($extra_info, array(
'key' => empty($options['key']) ? $this->get_key_remote() : $options['key'],
'name_indicator' => $this->key_name_indicator,
'url' => trailingslashit(network_site_url()),
'admin_url' => trailingslashit(admin_url()),
'network_admin_url' => trailingslashit(network_admin_url()),
if ('base64_with_count' == $format) {
$bundle = base64_encode(json_encode($bundle));
$len = strlen($bundle); // Get the length
$len = dechex($len); // The first bytes of the message are the bundle length
$len = str_pad($len, 4, '0', STR_PAD_LEFT); // Zero pad
public function set_key_local($key_local) {
$this->key_local = $key_local;
if ($this->key_option_name) update_site_option($this->key_option_name, $this->key_local);
public function generate_new_keypair($key_size = 2048) {
$this->ensure_crypto_loaded();
$keys = $rsa->createKey($key_size);
if (empty($keys['privatekey'])) {
$this->set_key_local(false);
$this->set_key_local($keys['privatekey']);
if (empty($keys['publickey'])) {
$this->set_key_remote(false);
$this->set_key_remote($keys['publickey']);
return empty($keys['publickey']) ? false : true;
* A base-64 encoded RSA hash (PKCS_1) of the message digest
* @param boolean $use_key
public function signature_for_message($message, $use_key = false) {
$hash_algorithm = 'sha256';
// Sign with the private (local) key
if (!$this->key_local) throw new Exception('No signing key has been set');
$use_key = $this->key_local;
$this->ensure_crypto_loaded();
// 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
$rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
// Don't do this: Crypt_RSA::sign() already calculates the digest of the hash
// $hash = new Crypt_Hash($hash_algorithm);
// $hashed = $hash->hash($message);
// if ($this->debug) $this->log("Message hash (hash=$hash_algorithm) (hex): ".bin2hex($hashed));
// phpseclib defaults to SHA1
$rsa->setHash($hash_algorithm);
$encrypted = $rsa->sign($message);
if ($this->debug) $this->log('Signed hash (mode='.CRYPT_RSA_SIGNATURE_PKCS1.') (hex): '.bin2hex($encrypted));
$signature = base64_encode($encrypted);
if ($this->debug) $this->log("Message signature (base64): $signature");
* @param string $level $level is not yet used much
private function log($message, $level = 'notice') {
// Allow other plugins to do something with the message
do_action('udrpc_log', $message, $level, $this->key_name_indicator, $this->debug, $this);
if ('info' != $level) error_log('UDRPC ('.$this->key_name_indicator.", $level): $message");
* Encrypt the message, using the local key (which needs to exist)
* @param string $plaintext
* @param boolean $use_key
* @param integer $key_length
public function encrypt_message($plaintext, $use_key = false, $key_length = 32) {
if (1 == $this->format) {
if (!$this->key_local) throw new Exception('No encryption key has been set');
$use_key = $this->key_local;
if (!$this->key_remote) throw new Exception('No encryption key has been set');
$use_key = $this->key_remote;
$this->ensure_crypto_loaded();
if (defined('UDRPC_PHPSECLIB_ENCRYPTION_MODE')) $rsa->setEncryptionMode(UDRPC_PHPSECLIB_ENCRYPTION_MODE);
$rij = new Crypt_Rijndael();
// Generate Random Symmetric Key
$sym_key = crypt_random_string($key_length);
if ($this->debug) $this->log('Unencrypted symmetric key (hex): '.bin2hex($sym_key));
// Encrypt Message with new Symmetric Key
$ciphertext = $rij->encrypt($plaintext);
if ($this->debug) $this->log('Encrypted ciphertext (hex): '.bin2hex($ciphertext));
$ciphertext = base64_encode($ciphertext);
// Encrypt the Symmetric Key with the Asymmetric Key
$sym_key = $rsa->encrypt($sym_key);
if ($this->debug) $this->log('Encrypted symmetric key (hex): '.bin2hex($sym_key));
// Base 64 encode the symmetric key for transport
$sym_key = base64_encode($sym_key);
if ($this->debug) $this->log('Encrypted symmetric key (b64): '.$sym_key);
$len = str_pad(dechex(strlen($sym_key)), 3, '0', STR_PAD_LEFT); // Zero pad to be sure.
// 16 characters of hex is enough for the payload to be to 16 exabytes (giga < tera < peta < exa) of data
$cipherlen = str_pad(dechex(strlen($ciphertext)), 16, '0', STR_PAD_LEFT);
// Concatenate the length, the encrypted symmetric key, and the message
return $len.$sym_key.$cipherlen.$ciphertext;
* Decrypt the message, using the local key (which needs to exist)
public function decrypt_message($message) {
if (!$this->key_local) throw new Exception('No decryption key has been set');
$this->ensure_crypto_loaded();
if (defined('UDRPC_PHPSECLIB_ENCRYPTION_MODE')) $rsa->setEncryptionMode(UDRPC_PHPSECLIB_ENCRYPTION_MODE);
// Defaults to CRYPT_AES_MODE_CBC
$rij = new Crypt_Rijndael();