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