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