3 * GNU Social - a federating social network
4 * Copyright (C) 2015, Free Software Foundation, Inc.
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Affero General Public License for more details.
16 * You should have received a copy of the GNU Affero General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 if (!defined('GNUSOCIAL')) { exit(1); }
23 * Diaspora federation protocol plugin for GNU Social
29 * @package ProtocolDiasporaPlugin
30 * @maintainer Mikael Nordfeldth <mmn@hethane.se>
33 // Depends on OStatus of course.
36 //Since Magicsig hasn't loaded yet
37 require_once('Crypt/AES.php');
39 class DiasporaPlugin extends Plugin
41 const REL_SEED_LOCATION = 'http://joindiaspora.com/seed_location';
42 const REL_GUID = 'http://joindiaspora.com/guid';
43 const REL_PUBLIC_KEY = 'diaspora-public-key';
45 public function onEndAttachPubkeyToUserXRD(Magicsig $magicsig, XML_XRD $xrd, Profile $target)
47 // So far we've only handled RSA keys, but it can change in the future,
48 // so be prepared. And remember to change the statically assigned type attribute below!
49 assert($magicsig->publicKey instanceof Crypt_RSA);
50 $xrd->links[] = new XML_XRD_Element_Link(self::REL_PUBLIC_KEY,
51 base64_encode($magicsig->exportPublicKey()), 'RSA');
53 // Instead of choosing a random string, we calculate our GUID from the public key
54 // by fingerprint through a sha256 hash.
55 $xrd->links[] = new XML_XRD_Element_Link(self::REL_GUID,
56 strtolower($magicsig->toFingerprint()));
59 public function onPluginVersion(array &$versions)
61 $versions[] = array('name' => 'Diaspora',
63 'author' => 'Mikael Nordfeldth',
64 'homepage' => 'https://gnu.io/social',
65 // TRANS: Plugin description.
66 'rawdescription' => _m('Follow people across social networks that implement '.
67 'the <a href="https://diasporafoundation.org/">Diaspora</a> federation protocol.'));
72 public function onStartMagicEnvelopeToXML(MagicEnvelope $magic_env, XMLStringer $xs, $flavour=null, Profile $target=null)
74 // Since Diaspora doesn't use a separate namespace for their "extended"
75 // salmon slap, we'll have to resort to this workaround hack.
76 if ($flavour !== 'diaspora') {
80 // WARNING: This changes the $magic_env contents! Be aware of it.
83 * https://wiki.diasporafoundation.org/Federation_protocol_overview
85 * Constructing the encryption header
89 * Choose an AES key and initialization vector, suitable for the
90 * aes-256-cbc cipher. I shall refer to this as the “inner key”
91 * and the “inner initialization vector (iv)”.
93 $inner_key = new Crypt_AES(CRYPT_AES_MODE_CBC);
94 $inner_key->setKeyLength(256); // set length to 256 bits (could be calculated, but let's be sure)
95 $inner_key->setKey(common_random_rawstr(32)); // 32 bytes from a (pseudo) random source
96 $inner_key->setIV(common_random_rawstr(16)); // 16 bytes is the block length
99 * Construct the following XML snippet:
101 * <iv>((base64-encoded inner iv))</iv>
102 * <aes_key>((base64-encoded inner key))</aes_key>
104 * <name>Alice Exampleman</name>
105 * <uri>acct:user@sender.example</uri>
107 * </decrypted_header>
109 $decrypted_header = sprintf('<decrypted_header><iv>%1$s</iv><aes_key>%2$s</aes_key><author_id>%3$s</author_id></decrypted_header>',
110 base64_encode($inner_key->iv),
111 base64_encode($inner_key->key),
112 $magic_env->getActor()->getAcctUri());
115 * Construct another AES key and initialization vector suitable
116 * for the aes-256-cbc cipher. I shall refer to this as the
117 * “outer key” and the “outer initialization vector (iv)”.
119 $outer_key = new Crypt_AES(CRYPT_AES_MODE_CBC);
120 $outer_key->setKeyLength(256); // set length to 256 bits (could be calculated, but let's be sure)
121 $outer_key->setKey(common_random_rawstr(32)); // 32 bytes from a (pseudo) random source
122 $outer_key->setIV(common_random_rawstr(16)); // 16 bytes is the block length
125 * Encrypt your <decrypted_header> XML snippet using the “outer key”
126 * and “outer iv” (using the aes-256-cbc cipher). This encrypted
127 * blob shall be referred to as “the ciphertext”.
129 $ciphertext = $outer_key->encrypt($decrypted_header);
132 * Construct the following JSON object, which shall be referred to
133 * as “the outer aes key bundle”:
135 * "iv": ((base64-encoded AES outer iv)),
136 * "key": ((base64-encoded AES outer key))
139 $outer_bundle = json_encode(array(
140 'iv' => base64_encode($outer_key->iv),
141 'key' => base64_encode($outer_key->key),
144 * Encrypt the “outer aes key bundle” with Bob’s RSA public key.
145 * I shall refer to this as the “encrypted outer aes key bundle”.
147 $key_fetcher = new MagicEnvelope();
148 $remote_keys = $key_fetcher->getKeyPair($target, true); // actually just gets the public key
149 $enc_outer = $remote_keys->publicKey->encrypt($outer_bundle);
152 * Construct the following JSON object, which I shall refer to as
153 * the “encrypted header json object”:
155 * "aes_key": ((base64-encoded encrypted outer aes key bundle)),
156 * "ciphertext": ((base64-encoded ciphertextm from above))
159 $enc_header = json_encode(array(
160 'aes_key' => base64_encode($enc_outer),
161 'ciphertext' => base64_encode($ciphertext),
165 * Construct the xml snippet:
166 * <encrypted_header>((base64-encoded encrypted header json object))</encrypted_header>
168 $xs->element('encrypted_header', null, base64_encode($enc_header));
171 * In order to prepare the payload message for inclusion in your
172 * salmon slap, you will:
174 * 1. Encrypt the payload message using the aes-256-cbc cipher and
175 * the “inner encryption key” and “inner encryption iv” you
177 * 2. Base64-encode the encrypted payload message.
179 $payload = $inner_key->encrypt($magic_env->getData());
180 $magic_env->signMessage(base64_encode($payload), 'application/xml');
183 // Since we have to change the content of me:data we'll just write the
184 // whole thing from scratch. We _could_ otherwise have just manipulated
185 // that element and added the encrypted_header in the EndMagicEnvelopeToXML event.
186 $xs->elementStart('me:env', array('xmlns:me' => MagicEnvelope::NS));
187 $xs->element('me:data', array('type' => $magic_env->getDataType()), $magic_env->getData());
188 $xs->element('me:encoding', null, $magic_env->getEncoding());
189 $xs->element('me:alg', null, $magic_env->getSignatureAlgorithm());
190 $xs->element('me:sig', null, $magic_env->getSignature());
191 $xs->elementEnd('me:env');
196 public function onSalmonSlap($endpoint_uri, MagicEnvelope $magic_env, Profile $target=null)
198 $envxml = $magic_env->toXML($target, 'diaspora');
200 // Diaspora wants another POST format (base64url-encoded POST variable 'xml')
201 $headers = array('Content-Type: application/x-www-form-urlencoded');
203 // Another way to distinguish Diaspora from GNU social is that a POST with
204 // $headers=array('Content-Type: application/magic-envelope+xml') would return
205 // HTTP status code 422 Unprocessable Entity, at least as of 2015-10-04.
207 $client = new HTTPClient();
208 $client->setBody('xml=' . Magicsig::base64_url_encode($envxml));
209 $response = $client->post($endpoint_uri, $headers);
210 } catch (HTTP_Request2_Exception $e) {
211 common_log(LOG_ERR, "Diaspora-flavoured Salmon post to $endpoint_uri failed: " . $e->getMessage());
215 // 200 OK is the best response
216 // 202 Accepted is what we get from Diaspora for example
217 if (!in_array($response->getStatus(), array(200, 202))) {
218 common_log(LOG_ERR, sprintf('Salmon (from profile %d) endpoint %s returned status %s: %s',
219 $user->id, $endpoint_uri, $response->getStatus(), $response->getBody()));