Fix indentation .. again ;-)
[friendica.git/.git] / src / Util / Crypto.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2020, Friendica
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Util;
23
24 use Friendica\Core\Hook;
25 use Friendica\Core\Logger;
26 use Friendica\Core\System;
27 use Friendica\DI;
28 use phpseclib\Crypt\RSA;
29 use phpseclib\Math\BigInteger;
30
31 /**
32  * Crypto class
33  */
34 class Crypto
35 {
36         // supported algorithms are 'sha256', 'sha1'
37         /**
38          * @param string $data data
39          * @param string $key  key
40          * @param string $alg  algorithm
41          * @return string
42          */
43         public static function rsaSign($data, $key, $alg = 'sha256')
44         {
45                 if (empty($key)) {
46                         Logger::warning('Empty key parameter', ['callstack' => System::callstack()]);
47                 }
48                 openssl_sign($data, $sig, $key, (($alg == 'sha1') ? OPENSSL_ALGO_SHA1 : $alg));
49                 return $sig;
50         }
51
52         /**
53          * @param string $data data
54          * @param string $sig  signature
55          * @param string $key  key
56          * @param string $alg  algorithm
57          * @return boolean
58          */
59         public static function rsaVerify($data, $sig, $key, $alg = 'sha256')
60         {
61                 if (empty($key)) {
62                         Logger::warning('Empty key parameter', ['callstack' => System::callstack()]);
63                 }
64                 return openssl_verify($data, $sig, $key, (($alg == 'sha1') ? OPENSSL_ALGO_SHA1 : $alg));
65         }
66
67         /**
68         /**
69          * @param string $m modulo
70          * @param string $e exponent
71          * @return string
72          */
73         public static function meToPem($m, $e)
74         {
75                 $rsa = new RSA();
76                 $rsa->loadKey([
77                         'e' => new BigInteger($e, 256),
78                         'n' => new BigInteger($m, 256)
79                 ]);
80                 return $rsa->getPublicKey();
81         }
82
83         /**
84          * Transform RSA public keys to standard PEM output
85          *
86          * @param string $key A RSA public key
87          *
88          * @return string The PEM output of this key
89          */
90         public static function rsaToPem(string $key)
91         {
92                 $rsa = new RSA();
93                 $rsa->setPublicKey($key);
94
95                 return $rsa->getPublicKey(RSA::PUBLIC_FORMAT_PKCS8);
96         }
97
98         /**
99          * Extracts the modulo and exponent reference from a public PEM key
100          *
101          * @param string $key      public PEM key
102          * @param string $modulus  (ref) modulo reference
103          * @param string $exponent (ref) exponent reference
104          *
105          * @return void
106          */
107         public static function pemToMe(string $key, &$modulus, &$exponent)
108         {
109                 $rsa = new RSA();
110                 $rsa->loadKey($key);
111                 $rsa->setPublicKey();
112
113                 $modulus  = $rsa->modulus->toBytes();
114                 $exponent = $rsa->exponent->toBytes();
115         }
116
117         /**
118          * @param integer $bits number of bits
119          * @return mixed
120          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
121          */
122         public static function newKeypair($bits)
123         {
124                 $openssl_options = [
125                         'digest_alg'       => 'sha1',
126                         'private_key_bits' => $bits,
127                         'encrypt_key'      => false
128                 ];
129
130                 $conf = DI::config()->get('system', 'openssl_conf_file');
131                 if ($conf) {
132                         $openssl_options['config'] = $conf;
133                 }
134                 $result = openssl_pkey_new($openssl_options);
135
136                 if (empty($result)) {
137                         Logger::log('new_keypair: failed');
138                         return false;
139                 }
140
141                 // Get private key
142                 $response = ['prvkey' => '', 'pubkey' => ''];
143
144                 openssl_pkey_export($result, $response['prvkey']);
145
146                 // Get public key
147                 $pkey = openssl_pkey_get_details($result);
148                 $response['pubkey'] = $pkey["key"];
149
150                 return $response;
151         }
152
153         /**
154          * Encrypt a string with 'aes-256-cbc' cipher method.
155          *
156          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
157          *
158          * @param string $data
159          * @param string $key   The key used for encryption.
160          * @param string $iv    A non-NULL Initialization Vector.
161          *
162          * @return string|boolean Encrypted string or false on failure.
163          */
164         private static function encryptAES256CBC($data, $key, $iv)
165         {
166                 return openssl_encrypt($data, 'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
167         }
168
169         /**
170          * Decrypt a string with 'aes-256-cbc' cipher method.
171          *
172          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
173          *
174          * @param string $data
175          * @param string $key   The key used for decryption.
176          * @param string $iv    A non-NULL Initialization Vector.
177          *
178          * @return string|boolean Decrypted string or false on failure.
179          */
180         private static function decryptAES256CBC($data, $key, $iv)
181         {
182                 return openssl_decrypt($data, 'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
183         }
184
185         /**
186          *
187          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
188          *
189          * @param string $data
190          * @param string $pubkey The public key.
191          * @param string $alg    The algorithm used for encryption.
192          *
193          * @return array
194          * @throws \Exception
195          */
196         public static function encapsulate($data, $pubkey, $alg = 'aes256cbc')
197         {
198                 if ($alg === 'aes256cbc') {
199                         return self::encapsulateAes($data, $pubkey);
200                 }
201                 return self::encapsulateOther($data, $pubkey, $alg);
202         }
203
204         /**
205          *
206          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
207          *
208          * @param string $data
209          * @param string $pubkey The public key.
210          * @param string $alg    The algorithm used for encryption.
211          *
212          * @return array
213          * @throws \Exception
214          */
215         private static function encapsulateOther($data, $pubkey, $alg)
216         {
217                 if (!$pubkey) {
218                         Logger::log('no key. data: '.$data);
219                 }
220                 $fn = 'encrypt' . strtoupper($alg);
221                 if (method_exists(__CLASS__, $fn)) {
222                         $result = ['encrypted' => true];
223                         $key = random_bytes(256);
224                         $iv  = random_bytes(256);
225                         $result['data'] = Strings::base64UrlEncode(self::$fn($data, $key, $iv), true);
226
227                         // log the offending call so we can track it down
228                         if (!openssl_public_encrypt($key, $k, $pubkey)) {
229                                 $x = debug_backtrace();
230                                 Logger::notice('RSA failed', ['trace' => $x[0]]);
231                         }
232
233                         $result['alg'] = $alg;
234                         $result['key'] = Strings::base64UrlEncode($k, true);
235                         openssl_public_encrypt($iv, $i, $pubkey);
236                         $result['iv'] = Strings::base64UrlEncode($i, true);
237
238                         return $result;
239                 } else {
240                         $x = ['data' => $data, 'pubkey' => $pubkey, 'alg' => $alg, 'result' => $data];
241                         Hook::callAll('other_encapsulate', $x);
242
243                         return $x['result'];
244                 }
245         }
246
247         /**
248          *
249          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
250          *
251          * @param string $data
252          * @param string $pubkey
253          *
254          * @return array
255          * @throws \Exception
256          */
257         private static function encapsulateAes($data, $pubkey)
258         {
259                 if (!$pubkey) {
260                         Logger::log('aes_encapsulate: no key. data: ' . $data);
261                 }
262
263                 $key = random_bytes(32);
264                 $iv  = random_bytes(16);
265                 $result = ['encrypted' => true];
266                 $result['data'] = Strings::base64UrlEncode(self::encryptAES256CBC($data, $key, $iv), true);
267
268                 // log the offending call so we can track it down
269                 if (!openssl_public_encrypt($key, $k, $pubkey)) {
270                         $x = debug_backtrace();
271                         Logger::log('aes_encapsulate: RSA failed. ' . print_r($x[0], true));
272                 }
273
274                 $result['alg'] = 'aes256cbc';
275                 $result['key'] = Strings::base64UrlEncode($k, true);
276                 openssl_public_encrypt($iv, $i, $pubkey);
277                 $result['iv'] = Strings::base64UrlEncode($i, true);
278
279                 return $result;
280         }
281
282         /**
283          *
284          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
285          *
286          * @param array $data ['iv' => $iv, 'key' => $key, 'alg' => $alg, 'data' => $data]
287          * @param string $prvkey The private key used for decryption.
288          *
289          * @return string|boolean The decrypted string or false on failure.
290          * @throws \Exception
291          */
292         public static function unencapsulate(array $data, $prvkey)
293         {
294                 if (!$data) {
295                         return;
296                 }
297
298                 $alg = $data['alg'] ?? 'aes256cbc';
299                 if ($alg === 'aes256cbc') {
300                         return self::unencapsulateAes($data['data'], $prvkey);
301                 }
302
303                 return self::unencapsulateOther($data, $prvkey, $alg);
304         }
305
306         /**
307          *
308          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
309          *
310          * @param array $data
311          * @param string $prvkey The private key used for decryption.
312          * @param string $alg
313          *
314          * @return string|boolean The decrypted string or false on failure.
315          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
316          */
317         private static function unencapsulateOther(array $data, $prvkey, $alg)
318         {
319                 $fn = 'decrypt' . strtoupper($alg);
320
321                 if (method_exists(__CLASS__, $fn)) {
322                         openssl_private_decrypt(Strings::base64UrlDecode($data['key']), $k, $prvkey);
323                         openssl_private_decrypt(Strings::base64UrlDecode($data['iv']), $i, $prvkey);
324
325                         return self::$fn(Strings::base64UrlDecode($data['data']), $k, $i);
326                 } else {
327                         $x = ['data' => $data, 'prvkey' => $prvkey, 'alg' => $alg, 'result' => $data];
328                         Hook::callAll('other_unencapsulate', $x);
329
330                         return $x['result'];
331                 }
332         }
333
334         /**
335          *
336          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
337          *
338          * @param array  $data
339          * @param string $prvkey The private key used for decryption.
340          *
341          * @return string|boolean The decrypted string or false on failure.
342          * @throws \Exception
343          */
344         private static function unencapsulateAes($data, $prvkey)
345         {
346                 openssl_private_decrypt(Strings::base64UrlDecode($data['key']), $k, $prvkey);
347                 openssl_private_decrypt(Strings::base64UrlDecode($data['iv']), $i, $prvkey);
348
349                 return self::decryptAES256CBC(Strings::base64UrlDecode($data['data']), $k, $i);
350         }
351
352
353         /**
354          * Creates cryptographic secure random digits
355          *
356          * @param string $digits The count of digits
357          * @return int The random Digits
358          *
359          * @throws \Exception In case 'random_int' isn't usable
360          */
361         public static function randomDigits($digits)
362         {
363                 $rn = '';
364
365                 // generating cryptographically secure pseudo-random integers
366                 for ($i = 0; $i < $digits; $i++) {
367                         $rn .= random_int(0, 9);
368                 }
369
370                 return $rn;
371         }
372 }