forked from GNUsocial/gnu-social
Diaspora plugin is almost there (for remote salmon slaps at least)
This commit is contained in:
parent
9b461db4da
commit
2aed59a02a
16
lib/util.php
16
lib/util.php
@ -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++) {
|
||||||
|
@ -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 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:
|
||||||
|
* <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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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.');
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user