]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Diaspora/DiasporaPlugin.php
376ad098b20dbfda085730646765f84e2b7d7aa6
[quix0rs-gnu-social.git] / plugins / Diaspora / DiasporaPlugin.php
1 <?php
2 /*
3  * GNU Social - a federating social network
4  * Copyright (C) 2015, Free Software Foundation, Inc.
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 if (!defined('GNUSOCIAL')) { exit(1); }
21
22 /**
23  * Diaspora federation protocol plugin for GNU Social
24  *
25  * Depends on:
26  *  - OStatus plugin
27  *  - WebFinger plugin
28  *
29  * @package ProtocolDiasporaPlugin
30  * @maintainer Mikael Nordfeldth <mmn@hethane.se>
31  */
32
33 // Depends on OStatus of course.
34 addPlugin('OStatus');
35
36 //Since Magicsig hasn't loaded yet
37 require_once('Crypt/AES.php');
38
39 class DiasporaPlugin extends Plugin
40 {
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';
44
45     public function onEndAttachPubkeyToUserXRD(Magicsig $magicsig, XML_XRD $xrd, Profile $target)
46     {
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');
52
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()));
57     }
58
59     public function onMagicsigPublicKeyFromXRD(XML_XRD $xrd, &$pubkey)
60     {
61         // See if we have a Diaspora public key in the XRD response
62         $link = $xrd->get(self::REL_PUBLIC_KEY, 'RSA');
63         if (!is_null($link)) {
64             // If we do, decode it so we have the PKCS1 format (starts with -----BEGIN PUBLIC KEY-----)
65             $pkcs1 = base64_decode($link->href);
66             $magicsig = new Magicsig(Magicsig::DEFAULT_SIGALG); // Diaspora uses RSA-SHA256 (we do too)
67             try {
68                 // Try to load the public key so we can get it in the standard Magic signature format
69                 $magicsig->loadPublicKeyPKCS1($pkcs1);
70                 // We found it and will now store it in $pubkey in a proper format!
71                 // This is how it would be found in a well implemented XRD according to the standard.
72                 $pubkey = 'data:application/magic-public-key,'.$magicsig->toString();
73                 common_debug('magic-public-key found in diaspora-public-key: '.$pubkey);
74                 return false;
75             } catch (ServerException $e) {
76                 common_log(LOG_WARNING, $e->getMessage());
77             }
78         }
79         return true;
80     }
81
82     public function onPluginVersion(array &$versions)
83     {
84         $versions[] = array('name' => 'Diaspora',
85                             'version' => '0.1',
86                             'author' => 'Mikael Nordfeldth',
87                             'homepage' => 'https://gnu.io/social',
88                             // TRANS: Plugin description.
89                             'rawdescription' => _m('Follow people across social networks that implement '.
90                                'the <a href="https://diasporafoundation.org/">Diaspora</a> federation protocol.'));
91
92         return true;
93     }
94
95     public function onStartMagicEnvelopeToXML(MagicEnvelope $magic_env, XMLStringer $xs, $flavour=null, Profile $target=null)
96     {
97         // Since Diaspora doesn't use a separate namespace for their "extended"
98         // salmon slap, we'll have to resort to this workaround hack.
99         if ($flavour !== 'diaspora') {
100             return true;
101         }
102
103         // WARNING: This changes the $magic_env contents! Be aware of it.
104
105         /**
106          * https://wiki.diasporafoundation.org/Federation_protocol_overview
107          * http://www.rubydoc.info/github/Raven24/diaspora-federation/master/DiasporaFederation/Salmon/EncryptedSlap
108          *
109          * Constructing the encryption header
110          */
111
112         // For some reason diaspora wants the salmon slap in a <diaspora> header.
113         $xs->elementStart('diaspora', array('xmlns'=>'https://joindiaspora.com/protocol'));
114
115         /**
116          * Choose an AES key and initialization vector, suitable for the
117          * aes-256-cbc cipher. I shall refer to this as the “inner key”
118          * and the “inner initialization vector (iv)”.
119          */
120         $inner_key = new Crypt_AES(CRYPT_AES_MODE_CBC);
121         $inner_key->setKeyLength(256);  // set length to 256 bits (could be calculated, but let's be sure)
122         $inner_key->setKey(common_random_rawstr(32));   // 32 bytes from a (pseudo) random source
123         $inner_key->setIV(common_random_rawstr(16));    // 16 bytes is the block length
124
125         /**
126          * Construct the following XML snippet:
127          *  <decrypted_header>
128          *      <iv>((base64-encoded inner iv))</iv>
129          *      <aes_key>((base64-encoded inner key))</aes_key>
130          *      <author>
131          *          <name>Alice Exampleman</name>
132          *          <uri>acct:user@sender.example</uri>
133          *      </author>
134          *  </decrypted_header>
135          */
136         $decrypted_header = sprintf('<decrypted_header><iv>%1$s</iv><aes_key>%2$s</aes_key><author_id>%3$s</author_id></decrypted_header>',
137                                     base64_encode($inner_key->iv),
138                                     base64_encode($inner_key->key),
139                                     $magic_env->getActor()->getAcctUri());
140
141         /**
142          * Construct another AES key and initialization vector suitable
143          * for the aes-256-cbc cipher. I shall refer to this as the
144          * “outer key” and the “outer initialization vector (iv)”.
145          */
146         $outer_key = new Crypt_AES(CRYPT_AES_MODE_CBC);
147         $outer_key->setKeyLength(256);  // set length to 256 bits (could be calculated, but let's be sure)
148         $outer_key->setKey(common_random_rawstr(32));   // 32 bytes from a (pseudo) random source
149         $outer_key->setIV(common_random_rawstr(16));    // 16 bytes is the block length
150
151         /**
152          * Encrypt your <decrypted_header> XML snippet using the “outer key”
153          * and “outer iv” (using the aes-256-cbc cipher). This encrypted
154          * blob shall be referred to as “the ciphertext”. 
155          */
156         $ciphertext = $outer_key->encrypt($decrypted_header);
157
158         /**
159          * Construct the following JSON object, which shall be referred to
160          * as “the outer aes key bundle”:
161          *  {
162          *      "iv": ((base64-encoded AES outer iv)),
163          *      "key": ((base64-encoded AES outer key))
164          *  }
165          */
166         $outer_bundle = json_encode(array(
167                                 'iv' => base64_encode($outer_key->iv),
168                                 'key' => base64_encode($outer_key->key),
169                             ));
170         /**
171          * Encrypt the “outer aes key bundle” with Bob’s RSA public key.
172          * I shall refer to this as the “encrypted outer aes key bundle”.
173          */
174         common_debug('Diaspora creating "outer aes key bundle", will require magic-public-key');
175         $key_fetcher = new MagicEnvelope();
176         $remote_keys = $key_fetcher->getKeyPair($target, true); // actually just gets the public key
177         $enc_outer = $remote_keys->publicKey->encrypt($outer_bundle);
178
179         /**
180          * Construct the following JSON object, which I shall refer to as
181          * the “encrypted header json object”:
182          *  {
183          *      "aes_key": ((base64-encoded encrypted outer aes key bundle)),
184          *      "ciphertext": ((base64-encoded ciphertextm from above))
185          *  }
186          */
187         $enc_header = json_encode(array(
188                             'aes_key' => base64_encode($enc_outer),
189                             'ciphertext' => base64_encode($ciphertext),
190                         ));
191
192         /**
193          * Construct the xml snippet:
194          *  <encrypted_header>((base64-encoded encrypted header json object))</encrypted_header>
195          */
196         $xs->element('encrypted_header', null, base64_encode($enc_header));
197
198         /**
199          * In order to prepare the payload message for inclusion in your
200          * salmon slap, you will:
201          *
202          * 1. Encrypt the payload message using the aes-256-cbc cipher and
203          *      the “inner encryption key” and “inner encryption iv” you
204          *      chose earlier.
205          * 2. Base64-encode the encrypted payload message.
206          */
207         $payload = $inner_key->encrypt($magic_env->getData());
208         //FIXME: This means we don't actually put an <atom:entry> in the payload,
209         // since Diaspora has its own update method! Silly me. Read up on:
210         // https://wiki.diasporafoundation.org/Federation_Message_Semantics
211         $magic_env->signMessage(base64_encode($payload), 'application/xml');
212
213
214         // Since we have to change the content of me:data we'll just write the
215         // whole thing from scratch. We _could_ otherwise have just manipulated
216         // that element and added the encrypted_header in the EndMagicEnvelopeToXML event.
217         $xs->elementStart('me:env', array('xmlns:me' => MagicEnvelope::NS));
218         $xs->element('me:data', array('type' => $magic_env->getDataType()), $magic_env->getData());
219         $xs->element('me:encoding', null, $magic_env->getEncoding());
220         $xs->element('me:alg', null, $magic_env->getSignatureAlgorithm());
221         $xs->element('me:sig', null, $magic_env->getSignature());
222         $xs->elementEnd('me:env');
223
224         $xs->elementEnd('entry');
225
226         return false;
227     }
228
229     public function onSalmonSlap($endpoint_uri, MagicEnvelope $magic_env, Profile $target=null)
230     {
231         $envxml = $magic_env->toXML($target, 'diaspora');
232
233         // Diaspora wants another POST format (base64url-encoded POST variable 'xml')
234         $headers = array('Content-Type: application/x-www-form-urlencoded');
235
236         // Another way to distinguish Diaspora from GNU social is that a POST with
237         // $headers=array('Content-Type: application/magic-envelope+xml') would return
238         // HTTP status code 422 Unprocessable Entity, at least as of 2015-10-04.
239         try {
240             $client = new HTTPClient();
241             $client->setBody('xml=' . Magicsig::base64_url_encode($envxml));
242             $response = $client->post($endpoint_uri, $headers);
243         } catch (HTTP_Request2_Exception $e) {
244             common_log(LOG_ERR, "Diaspora-flavoured Salmon post to $endpoint_uri failed: " . $e->getMessage());
245             return false;
246         }
247
248         // 200 OK is the best response
249         // 202 Accepted is what we get from Diaspora for example
250         if (!in_array($response->getStatus(), array(200, 202))) {
251             common_log(LOG_ERR, sprintf('Salmon (from profile %d) endpoint %s returned status %s: %s',
252                                 $magic_env->getActor()->getID(), $endpoint_uri, $response->getStatus(), $response->getBody()));
253             return true;
254         }
255
256         // Success!
257         return false;
258     }
259 }