]> git.mxchange.org Git - friendica.git/blob - src/Util/Crypto.php
Merge pull request #11519 from MrPetovan/task/11511-console-domain-move
[friendica.git] / src / Util / Crypto.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2022, the Friendica project
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 Exception;
25 use Friendica\Core\Hook;
26 use Friendica\Core\Logger;
27 use Friendica\Core\System;
28 use Friendica\DI;
29 use ParagonIE\ConstantTime\Base64UrlSafe;
30 use phpseclib\Crypt\RSA;
31 use phpseclib\Math\BigInteger;
32
33 /**
34  * Crypto class
35  */
36 class Crypto
37 {
38         // supported algorithms are 'sha256', 'sha1'
39         /**
40          * @param string $data data
41          * @param string $key  key
42          * @param string $alg  algorithm
43          * @return string
44          */
45         public static function rsaSign($data, $key, $alg = 'sha256')
46         {
47                 if (empty($key)) {
48                         Logger::warning('Empty key parameter', ['callstack' => System::callstack()]);
49                 }
50                 openssl_sign($data, $sig, $key, (($alg == 'sha1') ? OPENSSL_ALGO_SHA1 : $alg));
51                 return $sig;
52         }
53
54         /**
55          * @param string $data data
56          * @param string $sig  signature
57          * @param string $key  key
58          * @param string $alg  algorithm
59          * @return boolean
60          */
61         public static function rsaVerify($data, $sig, $key, $alg = 'sha256')
62         {
63                 if (empty($key)) {
64                         Logger::warning('Empty key parameter', ['callstack' => System::callstack()]);
65                 }
66                 return openssl_verify($data, $sig, $key, (($alg == 'sha1') ? OPENSSL_ALGO_SHA1 : $alg));
67         }
68
69         /**
70         /**
71          * @param string $m modulo
72          * @param string $e exponent
73          * @return string
74          */
75         public static function meToPem($m, $e)
76         {
77                 $rsa = new RSA();
78                 $rsa->loadKey([
79                         'e' => new BigInteger($e, 256),
80                         'n' => new BigInteger($m, 256)
81                 ]);
82                 return $rsa->getPublicKey();
83         }
84
85         /**
86          * Transform RSA public keys to standard PEM output
87          *
88          * @param string $key A RSA public key
89          *
90          * @return string The PEM output of this key
91          */
92         public static function rsaToPem(string $key)
93         {
94                 $rsa = new RSA();
95                 $rsa->setPublicKey($key);
96
97                 return $rsa->getPublicKey(RSA::PUBLIC_FORMAT_PKCS8);
98         }
99
100         /**
101          * Extracts the modulo and exponent reference from a public PEM key
102          *
103          * @param string $key      public PEM key
104          * @param string $modulus  (ref) modulo reference
105          * @param string $exponent (ref) exponent reference
106          *
107          * @return void
108          */
109         public static function pemToMe(string $key, &$modulus, &$exponent)
110         {
111                 $rsa = new RSA();
112                 $rsa->loadKey($key);
113                 $rsa->setPublicKey();
114
115                 $modulus  = $rsa->modulus->toBytes();
116                 $exponent = $rsa->exponent->toBytes();
117         }
118
119         /**
120          * @param integer $bits number of bits
121          * @return mixed
122          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
123          */
124         public static function newKeypair($bits)
125         {
126                 $openssl_options = [
127                         'digest_alg'       => 'sha1',
128                         'private_key_bits' => $bits,
129                         'encrypt_key'      => false
130                 ];
131
132                 $conf = DI::config()->get('system', 'openssl_conf_file');
133                 if ($conf) {
134                         $openssl_options['config'] = $conf;
135                 }
136                 $result = openssl_pkey_new($openssl_options);
137
138                 if (empty($result)) {
139                         Logger::notice('new_keypair: failed');
140                         return false;
141                 }
142
143                 // Get private key
144                 $response = ['prvkey' => '', 'pubkey' => ''];
145
146                 openssl_pkey_export($result, $response['prvkey']);
147
148                 // Get public key
149                 $pkey = openssl_pkey_get_details($result);
150                 $response['pubkey'] = $pkey["key"];
151
152                 return $response;
153         }
154
155         /**
156          * Create a new elliptic curve key pair
157          *
158          * @return array with the elements "prvkey", "pubkey", "vapid-public" and "vapid-private"
159          */
160         public static function newECKeypair()
161         {
162                 $openssl_options = [
163                         'curve_name'       => 'prime256v1',
164                         'private_key_type' => OPENSSL_KEYTYPE_EC
165                 ];
166
167                 $conf = DI::config()->get('system', 'openssl_conf_file');
168                 if ($conf) {
169                         $openssl_options['config'] = $conf;
170                 }
171                 $result = openssl_pkey_new($openssl_options);
172
173                 if (empty($result)) {
174                         throw new Exception('Key creation failed');
175                 }
176
177                 $response = ['prvkey' => '', 'pubkey' => ''];
178
179                 // Get private key
180                 openssl_pkey_export($result, $response['prvkey']);
181
182                 // Get public key
183                 $pkey = openssl_pkey_get_details($result);
184                 $response['pubkey'] = $pkey['key'];
185
186                 // Create VAPID keys
187                 // @see https://github.com/web-push-libs/web-push-php/blob/256a18b2a2411469c94943725fb6eccb9681bd75/src/Utils.php#L60-L62
188                 $hexString = '04';
189                 $hexString .= str_pad(bin2hex($pkey['ec']['x']), 64, '0', STR_PAD_LEFT);
190                 $hexString .= str_pad(bin2hex($pkey['ec']['y']), 64, '0', STR_PAD_LEFT);
191                 $response['vapid-public'] = Base64UrlSafe::encode(hex2bin($hexString));
192
193                 // @see https://github.com/web-push-libs/web-push-php/blob/256a18b2a2411469c94943725fb6eccb9681bd75/src/VAPID.php
194                 $response['vapid-private'] = Base64UrlSafe::encode(hex2bin(str_pad(bin2hex($pkey['ec']['d']), 64, '0', STR_PAD_LEFT)));
195
196                 return $response;
197         }
198
199         /**
200          * Encrypt a string with 'aes-256-cbc' cipher method.
201          *
202          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
203          *
204          * @param string $data
205          * @param string $key   The key used for encryption.
206          * @param string $iv    A non-NULL Initialization Vector.
207          *
208          * @return string|boolean Encrypted string or false on failure.
209          */
210         private static function encryptAES256CBC($data, $key, $iv)
211         {
212                 return openssl_encrypt($data, 'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
213         }
214
215         /**
216          * Decrypt a string with 'aes-256-cbc' cipher method.
217          *
218          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
219          *
220          * @param string $data
221          * @param string $key   The key used for decryption.
222          * @param string $iv    A non-NULL Initialization Vector.
223          *
224          * @return string|boolean Decrypted string or false on failure.
225          */
226         private static function decryptAES256CBC($data, $key, $iv)
227         {
228                 return openssl_decrypt($data, 'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
229         }
230
231         /**
232          *
233          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
234          *
235          * @param string $data
236          * @param string $pubkey The public key.
237          * @param string $alg    The algorithm used for encryption.
238          *
239          * @return array
240          * @throws \Exception
241          */
242         public static function encapsulate($data, $pubkey, $alg = 'aes256cbc')
243         {
244                 if ($alg === 'aes256cbc') {
245                         return self::encapsulateAes($data, $pubkey);
246                 }
247                 return self::encapsulateOther($data, $pubkey, $alg);
248         }
249
250         /**
251          *
252          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
253          *
254          * @param string $data
255          * @param string $pubkey The public key.
256          * @param string $alg    The algorithm used for encryption.
257          *
258          * @return array
259          * @throws \Exception
260          */
261         private static function encapsulateOther($data, $pubkey, $alg)
262         {
263                 if (!$pubkey) {
264                         Logger::notice('no key. data: '.$data);
265                 }
266                 $fn = 'encrypt' . strtoupper($alg);
267                 if (method_exists(__CLASS__, $fn)) {
268                         $result = ['encrypted' => true];
269                         $key = random_bytes(256);
270                         $iv  = random_bytes(256);
271                         $result['data'] = Strings::base64UrlEncode(self::$fn($data, $key, $iv), true);
272
273                         // log the offending call so we can track it down
274                         if (!openssl_public_encrypt($key, $k, $pubkey)) {
275                                 $x = debug_backtrace();
276                                 Logger::notice('RSA failed', ['trace' => $x[0]]);
277                         }
278
279                         $result['alg'] = $alg;
280                         $result['key'] = Strings::base64UrlEncode($k, true);
281                         openssl_public_encrypt($iv, $i, $pubkey);
282                         $result['iv'] = Strings::base64UrlEncode($i, true);
283
284                         return $result;
285                 } else {
286                         $x = ['data' => $data, 'pubkey' => $pubkey, 'alg' => $alg, 'result' => $data];
287                         Hook::callAll('other_encapsulate', $x);
288
289                         return $x['result'];
290                 }
291         }
292
293         /**
294          *
295          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
296          *
297          * @param string $data
298          * @param string $pubkey
299          *
300          * @return array
301          * @throws \Exception
302          */
303         private static function encapsulateAes($data, $pubkey)
304         {
305                 if (!$pubkey) {
306                         Logger::notice('aes_encapsulate: no key. data: ' . $data);
307                 }
308
309                 $key = random_bytes(32);
310                 $iv  = random_bytes(16);
311                 $result = ['encrypted' => true];
312                 $result['data'] = Strings::base64UrlEncode(self::encryptAES256CBC($data, $key, $iv), true);
313
314                 // log the offending call so we can track it down
315                 if (!openssl_public_encrypt($key, $k, $pubkey)) {
316                         $x = debug_backtrace();
317                         Logger::notice('aes_encapsulate: RSA failed.', ['data' => $x[0]]);
318                 }
319
320                 $result['alg'] = 'aes256cbc';
321                 $result['key'] = Strings::base64UrlEncode($k, true);
322                 openssl_public_encrypt($iv, $i, $pubkey);
323                 $result['iv'] = Strings::base64UrlEncode($i, true);
324
325                 return $result;
326         }
327
328         /**
329          *
330          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
331          *
332          * @param array $data ['iv' => $iv, 'key' => $key, 'alg' => $alg, 'data' => $data]
333          * @param string $prvkey The private key used for decryption.
334          *
335          * @return string|boolean The decrypted string or false on failure.
336          * @throws \Exception
337          */
338         public static function unencapsulate(array $data, $prvkey)
339         {
340                 if (!$data) {
341                         return;
342                 }
343
344                 $alg = $data['alg'] ?? 'aes256cbc';
345                 if ($alg === 'aes256cbc') {
346                         return self::unencapsulateAes($data['data'], $prvkey);
347                 }
348
349                 return self::unencapsulateOther($data, $prvkey, $alg);
350         }
351
352         /**
353          *
354          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
355          *
356          * @param array $data
357          * @param string $prvkey The private key used for decryption.
358          * @param string $alg
359          *
360          * @return string|boolean The decrypted string or false on failure.
361          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
362          */
363         private static function unencapsulateOther(array $data, $prvkey, $alg)
364         {
365                 $fn = 'decrypt' . strtoupper($alg);
366
367                 if (method_exists(__CLASS__, $fn)) {
368                         openssl_private_decrypt(Strings::base64UrlDecode($data['key']), $k, $prvkey);
369                         openssl_private_decrypt(Strings::base64UrlDecode($data['iv']), $i, $prvkey);
370
371                         return self::$fn(Strings::base64UrlDecode($data['data']), $k, $i);
372                 } else {
373                         $x = ['data' => $data, 'prvkey' => $prvkey, 'alg' => $alg, 'result' => $data];
374                         Hook::callAll('other_unencapsulate', $x);
375
376                         return $x['result'];
377                 }
378         }
379
380         /**
381          *
382          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
383          *
384          * @param array  $data
385          * @param string $prvkey The private key used for decryption.
386          *
387          * @return string|boolean The decrypted string or false on failure.
388          * @throws \Exception
389          */
390         private static function unencapsulateAes($data, $prvkey)
391         {
392                 openssl_private_decrypt(Strings::base64UrlDecode($data['key']), $k, $prvkey);
393                 openssl_private_decrypt(Strings::base64UrlDecode($data['iv']), $i, $prvkey);
394
395                 return self::decryptAES256CBC(Strings::base64UrlDecode($data['data']), $k, $i);
396         }
397
398
399         /**
400          * Creates cryptographic secure random digits
401          *
402          * @param string $digits The count of digits
403          * @return int The random Digits
404          *
405          * @throws \Exception In case 'random_int' isn't usable
406          */
407         public static function randomDigits($digits)
408         {
409                 $rn = '';
410
411                 // generating cryptographically secure pseudo-random integers
412                 for ($i = 0; $i < $digits; $i++) {
413                         $rn .= random_int(0, 9);
414                 }
415
416                 return $rn;
417         }
418 }