]> git.mxchange.org Git - friendica.git/blob - src/Util/Crypto.php
Merge pull request #8819 from tobiasd/2020.06-CHANGELOG
[friendica.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 ASN_BASE;
25 use ASNValue;
26 use Friendica\Core\Hook;
27 use Friendica\Core\Logger;
28 use Friendica\Core\System;
29 use Friendica\DI;
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          * @param string $Der     der formatted string
69          * @param bool   $Private key type optional, default false
70          * @return string
71          */
72         private static function DerToPem($Der, $Private = false)
73         {
74                 //Encode:
75                 $Der = base64_encode($Der);
76                 //Split lines:
77                 $lines = str_split($Der, 65);
78                 $body = implode("\n", $lines);
79                 //Get title:
80                 $title = $Private ? 'RSA PRIVATE KEY' : 'PUBLIC KEY';
81                 //Add wrapping:
82                 $result = "-----BEGIN {$title}-----\n";
83                 $result .= $body . "\n";
84                 $result .= "-----END {$title}-----\n";
85
86                 return $result;
87         }
88
89         /**
90          * @param string $Der der formatted string
91          * @return string
92          */
93         private static function DerToRsa($Der)
94         {
95                 //Encode:
96                 $Der = base64_encode($Der);
97                 //Split lines:
98                 $lines = str_split($Der, 64);
99                 $body = implode("\n", $lines);
100                 //Get title:
101                 $title = 'RSA PUBLIC KEY';
102                 //Add wrapping:
103                 $result = "-----BEGIN {$title}-----\n";
104                 $result .= $body . "\n";
105                 $result .= "-----END {$title}-----\n";
106
107                 return $result;
108         }
109
110         /**
111          * @param string $Modulus        modulo
112          * @param string $PublicExponent exponent
113          * @return string
114          */
115         private static function pkcs8Encode($Modulus, $PublicExponent)
116         {
117                 //Encode key sequence
118                 $modulus = new ASNValue(ASNValue::TAG_INTEGER);
119                 $modulus->SetIntBuffer($Modulus);
120                 $publicExponent = new ASNValue(ASNValue::TAG_INTEGER);
121                 $publicExponent->SetIntBuffer($PublicExponent);
122                 $keySequenceItems = [$modulus, $publicExponent];
123                 $keySequence = new ASNValue(ASNValue::TAG_SEQUENCE);
124                 $keySequence->SetSequence($keySequenceItems);
125                 //Encode bit string
126                 $bitStringValue = $keySequence->Encode();
127                 $bitStringValue = chr(0x00) . $bitStringValue; //Add unused bits byte
128                 $bitString = new ASNValue(ASNValue::TAG_BITSTRING);
129                 $bitString->Value = $bitStringValue;
130                 //Encode body
131                 $bodyValue = "\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05\x00" . $bitString->Encode();
132                 $body = new ASNValue(ASNValue::TAG_SEQUENCE);
133                 $body->Value = $bodyValue;
134                 //Get DER encoded public key:
135                 $PublicDER = $body->Encode();
136                 return $PublicDER;
137         }
138
139         /**
140          * @param string $Modulus        modulo
141          * @param string $PublicExponent exponent
142          * @return string
143          */
144         private static function pkcs1Encode($Modulus, $PublicExponent)
145         {
146                 //Encode key sequence
147                 $modulus = new ASNValue(ASNValue::TAG_INTEGER);
148                 $modulus->SetIntBuffer($Modulus);
149                 $publicExponent = new ASNValue(ASNValue::TAG_INTEGER);
150                 $publicExponent->SetIntBuffer($PublicExponent);
151                 $keySequenceItems = [$modulus, $publicExponent];
152                 $keySequence = new ASNValue(ASNValue::TAG_SEQUENCE);
153                 $keySequence->SetSequence($keySequenceItems);
154                 //Encode bit string
155                 $bitStringValue = $keySequence->Encode();
156                 return $bitStringValue;
157         }
158
159         /**
160          * @param string $m modulo
161          * @param string $e exponent
162          * @return string
163          */
164         public static function meToPem($m, $e)
165         {
166                 $der = self::pkcs8Encode($m, $e);
167                 $key = self::DerToPem($der, false);
168                 return $key;
169         }
170
171         /**
172          * @param string $key key
173          * @param string $m   modulo reference
174          * @param object $e   exponent reference
175          * @return void
176          * @throws \Exception
177          */
178         private static function pubRsaToMe($key, &$m, &$e)
179         {
180                 $lines = explode("\n", $key);
181                 unset($lines[0]);
182                 unset($lines[count($lines)]);
183                 $x = base64_decode(implode('', $lines));
184
185                 $r = ASN_BASE::parseASNString($x);
186
187                 $m = Strings::base64UrlDecode($r[0]->asnData[0]->asnData);
188                 $e = Strings::base64UrlDecode($r[0]->asnData[1]->asnData);
189         }
190
191         /**
192          * @param string $key key
193          * @return string
194          * @throws \Exception
195          */
196         public static function rsaToPem($key)
197         {
198                 self::pubRsaToMe($key, $m, $e);
199                 return self::meToPem($m, $e);
200         }
201
202         /**
203          * @param string $key key
204          * @return string
205          * @throws \Exception
206          */
207         private static function pemToRsa($key)
208         {
209                 self::pemToMe($key, $m, $e);
210                 return self::meToRsa($m, $e);
211         }
212
213         /**
214          * @param string $key key
215          * @param string $m   modulo reference
216          * @param string $e   exponent reference
217          * @return void
218          * @throws \Exception
219          */
220         public static function pemToMe($key, &$m, &$e)
221         {
222                 $lines = explode("\n", $key);
223                 unset($lines[0]);
224                 unset($lines[count($lines)]);
225                 $x = base64_decode(implode('', $lines));
226
227                 $r = ASN_BASE::parseASNString($x);
228
229                 if (isset($r[0])) {
230                         $m = Strings::base64UrlDecode($r[0]->asnData[1]->asnData[0]->asnData[0]->asnData);
231                         $e = Strings::base64UrlDecode($r[0]->asnData[1]->asnData[0]->asnData[1]->asnData);
232                 }
233         }
234
235         /**
236          * @param string $m modulo
237          * @param string $e exponent
238          * @return string
239          */
240         private static function meToRsa($m, $e)
241         {
242                 $der = self::pkcs1Encode($m, $e);
243                 $key = self::DerToRsa($der);
244                 return $key;
245         }
246
247         /**
248          * @param integer $bits number of bits
249          * @return mixed
250          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
251          */
252         public static function newKeypair($bits)
253         {
254                 $openssl_options = [
255                         'digest_alg'       => 'sha1',
256                         'private_key_bits' => $bits,
257                         'encrypt_key'      => false
258                 ];
259
260                 $conf = DI::config()->get('system', 'openssl_conf_file');
261                 if ($conf) {
262                         $openssl_options['config'] = $conf;
263                 }
264                 $result = openssl_pkey_new($openssl_options);
265
266                 if (empty($result)) {
267                         Logger::log('new_keypair: failed');
268                         return false;
269                 }
270
271                 // Get private key
272                 $response = ['prvkey' => '', 'pubkey' => ''];
273
274                 openssl_pkey_export($result, $response['prvkey']);
275
276                 // Get public key
277                 $pkey = openssl_pkey_get_details($result);
278                 $response['pubkey'] = $pkey["key"];
279
280                 return $response;
281         }
282
283         /**
284          * Encrypt a string with 'aes-256-cbc' cipher method.
285          * 
286          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
287          * 
288          * @param string $data
289          * @param string $key   The key used for encryption.
290          * @param string $iv    A non-NULL Initialization Vector.
291          * 
292          * @return string|boolean Encrypted string or false on failure.
293          */
294         private static function encryptAES256CBC($data, $key, $iv)
295         {
296                 return openssl_encrypt($data, 'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
297         }
298
299         /**
300          * Decrypt a string with 'aes-256-cbc' cipher method.
301          * 
302          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
303          * 
304          * @param string $data
305          * @param string $key   The key used for decryption.
306          * @param string $iv    A non-NULL Initialization Vector.
307          * 
308          * @return string|boolean Decrypted string or false on failure.
309          */
310         private static function decryptAES256CBC($data, $key, $iv)
311         {
312                 return openssl_decrypt($data, 'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
313         }
314
315         /**
316          * Encrypt a string with 'aes-256-ctr' cipher method.
317          * 
318          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
319          * 
320          * @param string $data
321          * @param string $key   The key used for encryption.
322          * @param string $iv    A non-NULL Initialization Vector.
323          * 
324          * @return string|boolean Encrypted string or false on failure.
325          */
326         private static function encryptAES256CTR($data, $key, $iv)
327         {
328                 $key = substr($key, 0, 32);
329                 $iv = substr($iv, 0, 16);
330                 return openssl_encrypt($data, 'aes-256-ctr', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
331         }
332
333         /**
334          * Decrypt a string with 'aes-256-ctr' cipher method.
335          * 
336          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
337          * 
338          * @param string $data
339          * @param string $key   The key used for decryption.
340          * @param string $iv    A non-NULL Initialization Vector.
341          * 
342          * @return string|boolean Decrypted string or false on failure.
343          */
344         private static function decryptAES256CTR($data, $key, $iv)
345         {
346                 $key = substr($key, 0, 32);
347                 $iv = substr($iv, 0, 16);
348                 return openssl_decrypt($data, 'aes-256-ctr', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
349         }
350
351         /**
352          *
353          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
354          *
355          * @param string $data
356          * @param string $pubkey The public key.
357          * @param string $alg    The algorithm used for encryption.
358          *
359          * @return array
360          * @throws \Exception
361          */
362         public static function encapsulate($data, $pubkey, $alg = 'aes256cbc')
363         {
364                 if ($alg === 'aes256cbc') {
365                         return self::encapsulateAes($data, $pubkey);
366                 }
367                 return self::encapsulateOther($data, $pubkey, $alg);
368         }
369
370         /**
371          *
372          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
373          *
374          * @param string $data
375          * @param string $pubkey The public key.
376          * @param string $alg    The algorithm used for encryption.
377          *
378          * @return array
379          * @throws \Exception
380          */
381         private static function encapsulateOther($data, $pubkey, $alg)
382         {
383                 if (!$pubkey) {
384                         Logger::log('no key. data: '.$data);
385                 }
386                 $fn = 'encrypt' . strtoupper($alg);
387                 if (method_exists(__CLASS__, $fn)) {
388                         $result = ['encrypted' => true];
389                         $key = random_bytes(256);
390                         $iv  = random_bytes(256);
391                         $result['data'] = Strings::base64UrlEncode(self::$fn($data, $key, $iv), true);
392
393                         // log the offending call so we can track it down
394                         if (!openssl_public_encrypt($key, $k, $pubkey)) {
395                                 $x = debug_backtrace();
396                                 Logger::notice('RSA failed', ['trace' => $x[0]]);
397                         }
398
399                         $result['alg'] = $alg;
400                         $result['key'] = Strings::base64UrlEncode($k, true);
401                         openssl_public_encrypt($iv, $i, $pubkey);
402                         $result['iv'] = Strings::base64UrlEncode($i, true);
403
404                         return $result;
405                 } else {
406                         $x = ['data' => $data, 'pubkey' => $pubkey, 'alg' => $alg, 'result' => $data];
407                         Hook::callAll('other_encapsulate', $x);
408
409                         return $x['result'];
410                 }
411         }
412
413         /**
414          *
415          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
416          *
417          * @param string $data
418          * @param string $pubkey
419          *
420          * @return array
421          * @throws \Exception
422          */
423         private static function encapsulateAes($data, $pubkey)
424         {
425                 if (!$pubkey) {
426                         Logger::log('aes_encapsulate: no key. data: ' . $data);
427                 }
428
429                 $key = random_bytes(32);
430                 $iv  = random_bytes(16);
431                 $result = ['encrypted' => true];
432                 $result['data'] = Strings::base64UrlEncode(self::encryptAES256CBC($data, $key, $iv), true);
433
434                 // log the offending call so we can track it down
435                 if (!openssl_public_encrypt($key, $k, $pubkey)) {
436                         $x = debug_backtrace();
437                         Logger::log('aes_encapsulate: RSA failed. ' . print_r($x[0], true));
438                 }
439
440                 $result['alg'] = 'aes256cbc';
441                 $result['key'] = Strings::base64UrlEncode($k, true);
442                 openssl_public_encrypt($iv, $i, $pubkey);
443                 $result['iv'] = Strings::base64UrlEncode($i, true);
444
445                 return $result;
446         }
447
448         /**
449          *
450          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
451          *
452          * @param array $data ['iv' => $iv, 'key' => $key, 'alg' => $alg, 'data' => $data]
453          * @param string $prvkey The private key used for decryption.
454          *
455          * @return string|boolean The decrypted string or false on failure.
456          * @throws \Exception
457          */
458         public static function unencapsulate(array $data, $prvkey)
459         {
460                 if (!$data) {
461                         return;
462                 }
463
464                 $alg = ((array_key_exists('alg', $data)) ? $data['alg'] : 'aes256cbc');
465                 if ($alg === 'aes256cbc') {
466                         return self::encapsulateAes($data['data'], $prvkey);
467                 }
468                 return self::encapsulateOther($data['data'], $prvkey, $alg);
469         }
470
471         /**
472          *
473          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
474          *
475          * @param array $data
476          * @param string $prvkey The private key used for decryption.
477          * @param string $alg
478          *
479          * @return string|boolean The decrypted string or false on failure.
480          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
481          */
482         private static function unencapsulateOther(array $data, $prvkey, $alg)
483         {
484                 $fn = 'decrypt' . strtoupper($alg);
485
486                 if (method_exists(__CLASS__, $fn)) {
487                         openssl_private_decrypt(Strings::base64UrlDecode($data['key']), $k, $prvkey);
488                         openssl_private_decrypt(Strings::base64UrlDecode($data['iv']), $i, $prvkey);
489
490                         return self::$fn(Strings::base64UrlDecode($data['data']), $k, $i);
491                 } else {
492                         $x = ['data' => $data, 'prvkey' => $prvkey, 'alg' => $alg, 'result' => $data];
493                         Hook::callAll('other_unencapsulate', $x);
494
495                         return $x['result'];
496                 }
497         }
498
499         /**
500          *
501          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
502          *
503          * @param array  $data
504          * @param string $prvkey The private key used for decryption.
505          *
506          * @return string|boolean The decrypted string or false on failure.
507          * @throws \Exception
508          */
509         private static function unencapsulateAes($data, $prvkey)
510         {
511                 openssl_private_decrypt(Strings::base64UrlDecode($data['key']), $k, $prvkey);
512                 openssl_private_decrypt(Strings::base64UrlDecode($data['iv']), $i, $prvkey);
513
514                 return self::decryptAES256CBC(Strings::base64UrlDecode($data['data']), $k, $i);
515         }
516
517
518         /**
519          * Creates cryptographic secure random digits
520          *
521          * @param string $digits The count of digits
522          * @return int The random Digits
523          *
524          * @throws \Exception In case 'random_int' isn't usable
525          */
526         public static function randomDigits($digits)
527         {
528                 $rn = '';
529
530                 // generating cryptographically secure pseudo-random integers
531                 for ($i = 0; $i < $digits; $i++) {
532                         $rn .= random_int(0, 9);
533                 }
534
535                 return $rn;
536         }
537 }