From 2aed59a02af83af0021db6464bd3cd3626c09b71 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sun, 4 Oct 2015 12:06:48 +0200 Subject: [PATCH] Diaspora plugin is almost there (for remote salmon slaps at least) --- lib/util.php | 16 ++- plugins/Diaspora/DiasporaPlugin.php | 161 ++++++++++++++++++++++++++ plugins/OStatus/OStatusPlugin.php | 4 +- plugins/OStatus/lib/magicenvelope.php | 14 ++- plugins/OStatus/lib/salmon.php | 5 +- 5 files changed, 190 insertions(+), 10 deletions(-) diff --git a/lib/util.php b/lib/util.php index edada03864..64733986d3 100644 --- a/lib/util.php +++ b/lib/util.php @@ -1556,14 +1556,24 @@ function common_root_url($ssl=false) return $url; } +/** + * returns $bytes bytes of raw random data + */ +function common_random_rawstr($bytes) +{ + $rawstr = @file_exists('/dev/urandom') + ? common_urandom($bytes) + : common_mtrand($bytes); + + return $rawstr; +} + /** * returns $bytes bytes of random data as a hexadecimal string */ function common_random_hexstr($bytes) { - $str = @file_exists('/dev/urandom') - ? common_urandom($bytes) - : common_mtrand($bytes); + $str = common_random_rawstr($bytes); $hexstr = ''; for ($i = 0; $i < $bytes; $i++) { diff --git a/plugins/Diaspora/DiasporaPlugin.php b/plugins/Diaspora/DiasporaPlugin.php index 66a9759f87..68f23a4382 100644 --- a/plugins/Diaspora/DiasporaPlugin.php +++ b/plugins/Diaspora/DiasporaPlugin.php @@ -30,6 +30,12 @@ if (!defined('GNUSOCIAL')) { exit(1); } * @maintainer Mikael Nordfeldth */ +// Depends on OStatus of course. +addPlugin('OStatus'); + +//Since Magicsig hasn't loaded yet +require_once('Crypt/AES.php'); + class DiasporaPlugin extends Plugin { const REL_SEED_LOCATION = 'http://joindiaspora.com/seed_location'; @@ -62,4 +68,159 @@ class DiasporaPlugin extends Plugin return true; } + + public function onStartMagicEnvelopeToXML(MagicEnvelope $magic_env, XMLStringer $xs, $flavour=null, Profile $target=null) + { + // Since Diaspora doesn't use a separate namespace for their "extended" + // salmon slap, we'll have to resort to this workaround hack. + if ($flavour !== 'diaspora') { + return true; + } + + // WARNING: This changes the $magic_env contents! Be aware of it. + + /** + * https://wiki.diasporafoundation.org/Federation_protocol_overview + * + * Constructing the encryption header + */ + + /** + * Choose an AES key and initialization vector, suitable for the + * aes-256-cbc cipher. I shall refer to this as the “inner key” + * and the “inner initialization vector (iv)”. + */ + $inner_key = new Crypt_AES(CRYPT_AES_MODE_CBC); + $inner_key->setKeyLength(256); // set length to 256 bits (could be calculated, but let's be sure) + $inner_key->setKey(common_random_rawstr(32)); // 32 bytes from a (pseudo) random source + $inner_key->setIV(common_random_rawstr(16)); // 16 bytes is the block length + + /** + * Construct the following XML snippet: + * + * ((base64-encoded inner iv)) + * ((base64-encoded inner key)) + * + * Alice Exampleman + * acct:user@sender.example + * + * + */ + $decrypted_header = sprintf('%1$s%2$s%3$s', + base64_encode($inner_key->iv), + base64_encode($inner_key->key), + $magic_env->getActor()->getAcctUri()); + + /** + * Construct another AES key and initialization vector suitable + * for the aes-256-cbc cipher. I shall refer to this as the + * “outer key” and the “outer initialization vector (iv)”. + */ + $outer_key = new Crypt_AES(CRYPT_AES_MODE_CBC); + $outer_key->setKeyLength(256); // set length to 256 bits (could be calculated, but let's be sure) + $outer_key->setKey(common_random_rawstr(32)); // 32 bytes from a (pseudo) random source + $outer_key->setIV(common_random_rawstr(16)); // 16 bytes is the block length + + /** + * Encrypt your XML snippet using the “outer key” + * and “outer iv” (using the aes-256-cbc cipher). This encrypted + * blob shall be referred to as “the ciphertext”. + */ + $ciphertext = $outer_key->encrypt($decrypted_header); + + /** + * Construct the following JSON object, which shall be referred to + * as “the outer aes key bundle”: + * { + * "iv": ((base64-encoded AES outer iv)), + * "key": ((base64-encoded AES outer key)) + * } + */ + $outer_bundle = json_encode(array( + 'iv' => base64_encode($outer_key->iv), + 'key' => base64_encode($outer_key->key), + )); + /** + * Encrypt the “outer aes key bundle” with Bob’s RSA public key. + * I shall refer to this as the “encrypted outer aes key bundle”. + */ + $key_fetcher = new MagicEnvelope(); + $remote_keys = $key_fetcher->getKeyPair($target, true); // actually just gets the public key + $enc_outer = $remote_keys->publicKey->encrypt($outer_bundle); + + /** + * Construct the following JSON object, which I shall refer to as + * the “encrypted header json object”: + * { + * "aes_key": ((base64-encoded encrypted outer aes key bundle)), + * "ciphertext": ((base64-encoded ciphertextm from above)) + * } + */ + $enc_header = json_encode(array( + 'aes_key' => base64_encode($enc_outer), + 'ciphertext' => base64_encode($ciphertext), + )); + + /** + * Construct the xml snippet: + * ((base64-encoded encrypted header json object)) + */ + $xs->element('encrypted_header', null, base64_encode($enc_header)); + + /** + * In order to prepare the payload message for inclusion in your + * salmon slap, you will: + * + * 1. Encrypt the payload message using the aes-256-cbc cipher and + * the “inner encryption key” and “inner encryption iv” you + * chose earlier. + * 2. Base64-encode the encrypted payload message. + */ + $payload = $inner_key->encrypt($magic_env->getData()); + $magic_env->signMessage(base64_encode($payload), 'application/xml'); + + + // Since we have to change the content of me:data we'll just write the + // whole thing from scratch. We _could_ otherwise have just manipulated + // that element and added the encrypted_header in the EndMagicEnvelopeToXML event. + $xs->elementStart('me:env', array('xmlns:me' => MagicEnvelope::NS)); + $xs->element('me:data', array('type' => $magic_env->getDataType()), $magic_env->getData()); + $xs->element('me:encoding', null, $magic_env->getEncoding()); + $xs->element('me:alg', null, $magic_env->getSignatureAlgorithm()); + $xs->element('me:sig', null, $magic_env->getSignature()); + $xs->elementEnd('me:env'); + + return false; + } + + public function onSalmonSlap($endpoint_uri, MagicEnvelope $magic_env, Profile $target=null) + { + $envxml = $magic_env->toXML($target, 'diaspora'); + + // Diaspora wants another POST format (base64url-encoded POST variable 'xml') + $headers = array('Content-Type: application/x-www-form-urlencoded'); + + // Another way to distinguish Diaspora from GNU social is that a POST with + // $headers=array('Content-Type: application/magic-envelope+xml') would return + // HTTP status code 422 Unprocessable Entity, at least as of 2015-10-04. + try { + $client = new HTTPClient(); + $client->setBody('xml=' . Magicsig::base64_url_encode($envxml)); + $response = $client->post($endpoint_uri, $headers); + } catch (HTTP_Request2_Exception $e) { + common_log(LOG_ERR, "Diaspora-flavoured Salmon post to $endpoint_uri failed: " . $e->getMessage()); + return false; + } + + // 200 OK is the best response + // 202 Accepted is what we get from Diaspora for example + if (!in_array($response->getStatus(), array(200, 202))) { + common_log(LOG_ERR, sprintf('Salmon (from profile %d) endpoint %s returned status %s: %s', + $user->id, $endpoint_uri, $response->getStatus(), $response->getBody())); + return true; + } + + // Success! + return false; + } } diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 24e877e262..5b7147ebee 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -1352,9 +1352,9 @@ class OStatusPlugin extends Plugin return true; } - public function onSalmonSlap($endpoint_uri, MagicEnvelope $magic_env) + public function onSalmonSlap($endpoint_uri, MagicEnvelope $magic_env, Profile $target=null) { - $envxml = $magic_env->toXML(); + $envxml = $magic_env->toXML($target); $headers = array('Content-Type: application/magic-envelope+xml'); diff --git a/plugins/OStatus/lib/magicenvelope.php b/plugins/OStatus/lib/magicenvelope.php index e6b068c924..e96862bbad 100644 --- a/plugins/OStatus/lib/magicenvelope.php +++ b/plugins/OStatus/lib/magicenvelope.php @@ -205,10 +205,10 @@ class MagicEnvelope * * @return string representation of XML document */ - public function toXML($flavour=null) { + public function toXML(Profile $target=null, $flavour=null) { $xs = new XMLStringer(); $xs->startXML(); // header, to point out it's not HTML or anything... - if (Event::handle('StartMagicEnvelopeToXML', array($this, $xs, $flavour))) { + if (Event::handle('StartMagicEnvelopeToXML', array($this, $xs, $flavour, $target))) { // fall back to our default, normal Magic Envelope XML. // the $xs element _may_ have had elements added, or could get in the end event $xs->elementStart('me:env', array('xmlns:me' => self::NS)); @@ -218,7 +218,7 @@ class MagicEnvelope $xs->element('me:sig', null, $this->getSignature()); $xs->elementEnd('me:env'); - Event::handle('EndMagicEnvelopeToXML', array($this, $xs, $flavour)); + Event::handle('EndMagicEnvelopeToXML', array($this, $xs, $flavour, $target)); } return $xs->getString(); } @@ -266,6 +266,9 @@ class MagicEnvelope public function getSignature() { + if (empty($this->sig)) { + throw new ServerException('You must first call signMessage before getSignature'); + } return $this->sig; } @@ -274,6 +277,11 @@ class MagicEnvelope return $this->alg; } + public function getData() + { + return $this->data; + } + public function getDataType() { return $this->data_type; diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php index 15ed123eed..cdc8e02d77 100644 --- a/plugins/OStatus/lib/salmon.php +++ b/plugins/OStatus/lib/salmon.php @@ -46,7 +46,7 @@ class Salmon * @param User $user local user profile whose keys we sign with * @return boolean success */ - public static function post($endpoint_uri, $xml, User $user) + public static function post($endpoint_uri, $xml, User $user, Profile $target=null) { if (empty($endpoint_uri)) { common_debug('No endpoint URI for Salmon post to '.$user->getUri()); @@ -60,7 +60,8 @@ class Salmon return false; } - if (Event::handle('SalmonSlap', array($magic_env))) { + // $target is so far only used in Diaspora, so it can be null + if (Event::handle('SalmonSlap', array($endpoint_uri, $magic_env, $target))) { return false; //throw new ServerException('Could not distribute salmon slap as no plugin completed the event.'); }