Diaspora plugin is almost there (for remote salmon slaps at least)

This commit is contained in:
Mikael Nordfeldth 2015-10-04 12:06:48 +02:00
parent 9b461db4da
commit 2aed59a02a
5 changed files with 190 additions and 10 deletions

View File

@ -1556,14 +1556,24 @@ function common_root_url($ssl=false)
return $url; 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 * returns $bytes bytes of random data as a hexadecimal string
*/ */
function common_random_hexstr($bytes) function common_random_hexstr($bytes)
{ {
$str = @file_exists('/dev/urandom') $str = common_random_rawstr($bytes);
? common_urandom($bytes)
: common_mtrand($bytes);
$hexstr = ''; $hexstr = '';
for ($i = 0; $i < $bytes; $i++) { for ($i = 0; $i < $bytes; $i++) {

View File

@ -30,6 +30,12 @@ if (!defined('GNUSOCIAL')) { exit(1); }
* @maintainer Mikael Nordfeldth <mmn@hethane.se> * @maintainer Mikael Nordfeldth <mmn@hethane.se>
*/ */
// Depends on OStatus of course.
addPlugin('OStatus');
//Since Magicsig hasn't loaded yet
require_once('Crypt/AES.php');
class DiasporaPlugin extends Plugin class DiasporaPlugin extends Plugin
{ {
const REL_SEED_LOCATION = 'http://joindiaspora.com/seed_location'; const REL_SEED_LOCATION = 'http://joindiaspora.com/seed_location';
@ -62,4 +68,159 @@ class DiasporaPlugin extends Plugin
return true; 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:
* <decrypted_header>
* <iv>((base64-encoded inner iv))</iv>
* <aes_key>((base64-encoded inner key))</aes_key>
* <author>
* <name>Alice Exampleman</name>
* <uri>acct:user@sender.example</uri>
* </author>
* </decrypted_header>
*/
$decrypted_header = sprintf('<decrypted_header><iv>%1$s</iv><aes_key>%2$s</aes_key><author_id>%3$s</author_id></decrypted_header>',
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 <decrypted_header> 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 Bobs 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:
* <encrypted_header>((base64-encoded encrypted header json object))</encrypted_header>
*/
$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;
}
} }

View File

@ -1352,9 +1352,9 @@ class OStatusPlugin extends Plugin
return true; 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'); $headers = array('Content-Type: application/magic-envelope+xml');

View File

@ -205,10 +205,10 @@ class MagicEnvelope
* *
* @return string representation of XML document * @return string representation of XML document
*/ */
public function toXML($flavour=null) { public function toXML(Profile $target=null, $flavour=null) {
$xs = new XMLStringer(); $xs = new XMLStringer();
$xs->startXML(); // header, to point out it's not HTML or anything... $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. // fall back to our default, normal Magic Envelope XML.
// the $xs element _may_ have had elements added, or could get in the end event // the $xs element _may_ have had elements added, or could get in the end event
$xs->elementStart('me:env', array('xmlns:me' => self::NS)); $xs->elementStart('me:env', array('xmlns:me' => self::NS));
@ -218,7 +218,7 @@ class MagicEnvelope
$xs->element('me:sig', null, $this->getSignature()); $xs->element('me:sig', null, $this->getSignature());
$xs->elementEnd('me:env'); $xs->elementEnd('me:env');
Event::handle('EndMagicEnvelopeToXML', array($this, $xs, $flavour)); Event::handle('EndMagicEnvelopeToXML', array($this, $xs, $flavour, $target));
} }
return $xs->getString(); return $xs->getString();
} }
@ -266,6 +266,9 @@ class MagicEnvelope
public function getSignature() public function getSignature()
{ {
if (empty($this->sig)) {
throw new ServerException('You must first call signMessage before getSignature');
}
return $this->sig; return $this->sig;
} }
@ -274,6 +277,11 @@ class MagicEnvelope
return $this->alg; return $this->alg;
} }
public function getData()
{
return $this->data;
}
public function getDataType() public function getDataType()
{ {
return $this->data_type; return $this->data_type;

View File

@ -46,7 +46,7 @@ class Salmon
* @param User $user local user profile whose keys we sign with * @param User $user local user profile whose keys we sign with
* @return boolean success * @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)) { if (empty($endpoint_uri)) {
common_debug('No endpoint URI for Salmon post to '.$user->getUri()); common_debug('No endpoint URI for Salmon post to '.$user->getUri());
@ -60,7 +60,8 @@ class Salmon
return false; 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; return false;
//throw new ServerException('Could not distribute salmon slap as no plugin completed the event.'); //throw new ServerException('Could not distribute salmon slap as no plugin completed the event.');
} }