* @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
{
const REL_SEED_LOCATION = 'http://joindiaspora.com/seed_location';
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;
+ }
}
*
* @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));
$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();
}
public function getSignature()
{
+ if (empty($this->sig)) {
+ throw new ServerException('You must first call signMessage before getSignature');
+ }
return $this->sig;
}
return $this->alg;
}
+ public function getData()
+ {
+ return $this->data;
+ }
+
public function getDataType()
{
return $this->data_type;