gnu-social/plugins/Diaspora/DiasporaPlugin.php

264 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/*
* GNU Social - a federating social network
* Copyright (C) 2015, Free Software Foundation, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('GNUSOCIAL')) { exit(1); }
/**
* Diaspora federation protocol plugin for GNU Social
*
* Depends on:
* - OStatus plugin
* - WebFinger plugin
*
* @package ProtocolDiasporaPlugin
* @maintainer Mikael Nordfeldth <mmn@hethane.se>
*/
// Depends on OStatus of course.
addPlugin('OStatus');
class DiasporaPlugin extends Plugin
{
const PLUGIN_VERSION = '0.2.0';
const REL_SEED_LOCATION = 'http://joindiaspora.com/seed_location';
const REL_GUID = 'http://joindiaspora.com/guid';
const REL_PUBLIC_KEY = 'diaspora-public-key';
public function onEndAttachPubkeyToUserXRD(Magicsig $magicsig, XML_XRD $xrd, Profile $target)
{
// So far we've only handled RSA keys, but it can change in the future,
// so be prepared. And remember to change the statically assigned type attribute below!
assert($magicsig->publicKey instanceof \phpseclib\Crypt\RSA);
$xrd->links[] = new XML_XRD_Element_Link(self::REL_PUBLIC_KEY,
base64_encode($magicsig->exportPublicKey()), 'RSA');
// Instead of choosing a random string, we calculate our GUID from the public key
// by fingerprint through a sha256 hash.
$xrd->links[] = new XML_XRD_Element_Link(self::REL_GUID,
strtolower($magicsig->toFingerprint()));
}
public function onMagicsigPublicKeyFromXRD(XML_XRD $xrd, &$pubkey)
{
// See if we have a Diaspora public key in the XRD response
$link = $xrd->get(self::REL_PUBLIC_KEY, 'RSA');
if (!is_null($link)) {
// If we do, decode it so we have the PKCS1 format (starts with -----BEGIN PUBLIC KEY-----)
$pkcs1 = base64_decode($link->href);
$magicsig = new Magicsig(Magicsig::DEFAULT_SIGALG); // Diaspora uses RSA-SHA256 (we do too)
try {
// Try to load the public key so we can get it in the standard Magic signature format
$magicsig->loadPublicKeyPKCS1($pkcs1);
// We found it and will now store it in $pubkey in a proper format!
// This is how it would be found in a well implemented XRD according to the standard.
$pubkey = 'data:application/magic-public-key,'.$magicsig->toString();
common_debug('magic-public-key found in diaspora-public-key: '.$pubkey);
return false;
} catch (ServerException $e) {
common_log(LOG_WARNING, $e->getMessage());
}
}
return true;
}
public function onPluginVersion(array &$versions): bool
{
$versions[] = array('name' => 'Diaspora',
'version' => self::PLUGIN_VERSION,
'author' => 'Mikael Nordfeldth',
'homepage' => GNUSOCIAL_ENGINE_URL,
// TRANS: Plugin description.
'rawdescription' => _m('Follow people across social networks that implement '.
'the <a href="https://diasporafoundation.org/">Diaspora</a> federation protocol.'));
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
* http://www.rubydoc.info/github/Raven24/diaspora-federation/master/DiasporaFederation/Salmon/EncryptedSlap
*
* Constructing the encryption header
*/
// For some reason diaspora wants the salmon slap in a <diaspora> header.
$xs->elementStart('diaspora', array('xmlns'=>'https://joindiaspora.com/protocol'));
/**
* 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 \phpseclib\Crypt\AES(\phpseclib\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 \phpseclib\Crypt\AES(\phpseclib\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, \phpseclib\Crypt\RSA::PADDING_PKCS1);
/**
* 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”.
*/
common_debug('Diaspora creating "outer aes key bundle", will require magic-public-key');
$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, \phpseclib\Crypt\RSA::PADDING_PKCS1);
/**
* 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(), \phpseclib\Crypt\RSA::PADDING_PKCS1);
//FIXME: This means we don't actually put an <atom:entry> in the payload,
// since Diaspora has its own update method! Silly me. Read up on:
// https://wiki.diasporafoundation.org/Federation_Message_Semantics
$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');
$xs->elementEnd('entry');
return false;
}
public function onSalmonSlap($endpoint_uri, MagicEnvelope $magic_env, Profile $target=null)
{
try {
$envxml = $magic_env->toXML($target, 'diaspora');
} catch (Exception $e) {
common_log(LOG_ERR, sprintf('Could not generate Magic Envelope XML (diaspora flavour) for profile id=='.$target->getID().': '.$e->getMessage()));
return false;
}
// 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 (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',
$magic_env->getActor()->getID(), $endpoint_uri, $response->getStatus(), $response->getBody()));
return true;
}
// Success!
return false;
}
}