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