Replace library/asn1.php with phpseclib
[friendica.git/.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 ASNValue;
25 use Friendica\Core\Hook;
26 use Friendica\Core\Logger;
27 use Friendica\Core\System;
28 use Friendica\DI;
29 use phpseclib\Crypt\RSA;
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 $Modulus        modulo
91          * @param string $PublicExponent exponent
92          * @return string
93          */
94         private static function pkcs8Encode($Modulus, $PublicExponent)
95         {
96                 //Encode key sequence
97                 $modulus = new ASNValue(ASNValue::TAG_INTEGER);
98                 $modulus->SetIntBuffer($Modulus);
99                 $publicExponent = new ASNValue(ASNValue::TAG_INTEGER);
100                 $publicExponent->SetIntBuffer($PublicExponent);
101                 $keySequenceItems = [$modulus, $publicExponent];
102                 $keySequence = new ASNValue(ASNValue::TAG_SEQUENCE);
103                 $keySequence->SetSequence($keySequenceItems);
104                 //Encode bit string
105                 $bitStringValue = $keySequence->Encode();
106                 $bitStringValue = chr(0x00) . $bitStringValue; //Add unused bits byte
107                 $bitString = new ASNValue(ASNValue::TAG_BITSTRING);
108                 $bitString->Value = $bitStringValue;
109                 //Encode body
110                 $bodyValue = "\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05\x00" . $bitString->Encode();
111                 $body = new ASNValue(ASNValue::TAG_SEQUENCE);
112                 $body->Value = $bodyValue;
113                 //Get DER encoded public key:
114                 $PublicDER = $body->Encode();
115                 return $PublicDER;
116         }
117
118         /**
119          * @param string $m modulo
120          * @param string $e exponent
121          * @return string
122          */
123         public static function meToPem($m, $e)
124         {
125                 $der = self::pkcs8Encode($m, $e);
126                 $key = self::DerToPem($der, false);
127                 return $key;
128         }
129
130         /**
131          * Transform RSA public keys to standard PEM output
132          *
133          * @param string $key A RSA public key
134          *
135          * @return string The PEM output of this key
136          */
137         public static function rsaToPem(string $key)
138         {
139                 $publicKey = new RSA();
140                 $publicKey->setPublicKey($key);
141
142                 return $publicKey->getPublicKey(RSA::PUBLIC_FORMAT_PKCS8);
143         }
144
145         /**
146          * Extracts the modulo and exponent reference from a public PEM key
147          *
148          * @param string $key      public PEM key
149          * @param string $modulus  (ref) modulo reference
150          * @param string $exponent (ref) exponent reference
151          *
152          * @return void
153          */
154         public static function pemToMe(string $key, string &$modulus, string &$exponent)
155         {
156                 $publicKey = new RSA();
157                 $publicKey->loadKey($key);
158                 $publicKey->setPublicKey();
159
160                 $modulus  = $publicKey->modulus->toBytes();
161                 $exponent = $publicKey->exponent->toBytes();
162         }
163
164         /**
165          * @param integer $bits number of bits
166          * @return mixed
167          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
168          */
169         public static function newKeypair($bits)
170         {
171                 $openssl_options = [
172                         'digest_alg'       => 'sha1',
173                         'private_key_bits' => $bits,
174                         'encrypt_key'      => false
175                 ];
176
177                 $conf = DI::config()->get('system', 'openssl_conf_file');
178                 if ($conf) {
179                         $openssl_options['config'] = $conf;
180                 }
181                 $result = openssl_pkey_new($openssl_options);
182
183                 if (empty($result)) {
184                         Logger::log('new_keypair: failed');
185                         return false;
186                 }
187
188                 // Get private key
189                 $response = ['prvkey' => '', 'pubkey' => ''];
190
191                 openssl_pkey_export($result, $response['prvkey']);
192
193                 // Get public key
194                 $pkey = openssl_pkey_get_details($result);
195                 $response['pubkey'] = $pkey["key"];
196
197                 return $response;
198         }
199
200         /**
201          * Encrypt a string with 'aes-256-cbc' cipher method.
202          * 
203          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
204          * 
205          * @param string $data
206          * @param string $key   The key used for encryption.
207          * @param string $iv    A non-NULL Initialization Vector.
208          * 
209          * @return string|boolean Encrypted string or false on failure.
210          */
211         private static function encryptAES256CBC($data, $key, $iv)
212         {
213                 return openssl_encrypt($data, 'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
214         }
215
216         /**
217          * Decrypt a string with 'aes-256-cbc' cipher method.
218          * 
219          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
220          * 
221          * @param string $data
222          * @param string $key   The key used for decryption.
223          * @param string $iv    A non-NULL Initialization Vector.
224          * 
225          * @return string|boolean Decrypted string or false on failure.
226          */
227         private static function decryptAES256CBC($data, $key, $iv)
228         {
229                 return openssl_decrypt($data, 'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
230         }
231
232         /**
233          *
234          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
235          *
236          * @param string $data
237          * @param string $pubkey The public key.
238          * @param string $alg    The algorithm used for encryption.
239          *
240          * @return array
241          * @throws \Exception
242          */
243         public static function encapsulate($data, $pubkey, $alg = 'aes256cbc')
244         {
245                 if ($alg === 'aes256cbc') {
246                         return self::encapsulateAes($data, $pubkey);
247                 }
248                 return self::encapsulateOther($data, $pubkey, $alg);
249         }
250
251         /**
252          *
253          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
254          *
255          * @param string $data
256          * @param string $pubkey The public key.
257          * @param string $alg    The algorithm used for encryption.
258          *
259          * @return array
260          * @throws \Exception
261          */
262         private static function encapsulateOther($data, $pubkey, $alg)
263         {
264                 if (!$pubkey) {
265                         Logger::log('no key. data: '.$data);
266                 }
267                 $fn = 'encrypt' . strtoupper($alg);
268                 if (method_exists(__CLASS__, $fn)) {
269                         $result = ['encrypted' => true];
270                         $key = random_bytes(256);
271                         $iv  = random_bytes(256);
272                         $result['data'] = Strings::base64UrlEncode(self::$fn($data, $key, $iv), true);
273
274                         // log the offending call so we can track it down
275                         if (!openssl_public_encrypt($key, $k, $pubkey)) {
276                                 $x = debug_backtrace();
277                                 Logger::notice('RSA failed', ['trace' => $x[0]]);
278                         }
279
280                         $result['alg'] = $alg;
281                         $result['key'] = Strings::base64UrlEncode($k, true);
282                         openssl_public_encrypt($iv, $i, $pubkey);
283                         $result['iv'] = Strings::base64UrlEncode($i, true);
284
285                         return $result;
286                 } else {
287                         $x = ['data' => $data, 'pubkey' => $pubkey, 'alg' => $alg, 'result' => $data];
288                         Hook::callAll('other_encapsulate', $x);
289
290                         return $x['result'];
291                 }
292         }
293
294         /**
295          *
296          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
297          *
298          * @param string $data
299          * @param string $pubkey
300          *
301          * @return array
302          * @throws \Exception
303          */
304         private static function encapsulateAes($data, $pubkey)
305         {
306                 if (!$pubkey) {
307                         Logger::log('aes_encapsulate: no key. data: ' . $data);
308                 }
309
310                 $key = random_bytes(32);
311                 $iv  = random_bytes(16);
312                 $result = ['encrypted' => true];
313                 $result['data'] = Strings::base64UrlEncode(self::encryptAES256CBC($data, $key, $iv), true);
314
315                 // log the offending call so we can track it down
316                 if (!openssl_public_encrypt($key, $k, $pubkey)) {
317                         $x = debug_backtrace();
318                         Logger::log('aes_encapsulate: RSA failed. ' . print_r($x[0], true));
319                 }
320
321                 $result['alg'] = 'aes256cbc';
322                 $result['key'] = Strings::base64UrlEncode($k, true);
323                 openssl_public_encrypt($iv, $i, $pubkey);
324                 $result['iv'] = Strings::base64UrlEncode($i, true);
325
326                 return $result;
327         }
328
329         /**
330          *
331          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
332          *
333          * @param array $data ['iv' => $iv, 'key' => $key, 'alg' => $alg, 'data' => $data]
334          * @param string $prvkey The private key used for decryption.
335          *
336          * @return string|boolean The decrypted string or false on failure.
337          * @throws \Exception
338          */
339         public static function unencapsulate(array $data, $prvkey)
340         {
341                 if (!$data) {
342                         return;
343                 }
344
345                 $alg = $data['alg'] ?? 'aes256cbc';
346                 if ($alg === 'aes256cbc') {
347                         return self::unencapsulateAes($data['data'], $prvkey);
348                 }
349
350                 return self::unencapsulateOther($data, $prvkey, $alg);
351         }
352
353         /**
354          *
355          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
356          *
357          * @param array $data
358          * @param string $prvkey The private key used for decryption.
359          * @param string $alg
360          *
361          * @return string|boolean The decrypted string or false on failure.
362          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
363          */
364         private static function unencapsulateOther(array $data, $prvkey, $alg)
365         {
366                 $fn = 'decrypt' . strtoupper($alg);
367
368                 if (method_exists(__CLASS__, $fn)) {
369                         openssl_private_decrypt(Strings::base64UrlDecode($data['key']), $k, $prvkey);
370                         openssl_private_decrypt(Strings::base64UrlDecode($data['iv']), $i, $prvkey);
371
372                         return self::$fn(Strings::base64UrlDecode($data['data']), $k, $i);
373                 } else {
374                         $x = ['data' => $data, 'prvkey' => $prvkey, 'alg' => $alg, 'result' => $data];
375                         Hook::callAll('other_unencapsulate', $x);
376
377                         return $x['result'];
378                 }
379         }
380
381         /**
382          *
383          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
384          *
385          * @param array  $data
386          * @param string $prvkey The private key used for decryption.
387          *
388          * @return string|boolean The decrypted string or false on failure.
389          * @throws \Exception
390          */
391         private static function unencapsulateAes($data, $prvkey)
392         {
393                 openssl_private_decrypt(Strings::base64UrlDecode($data['key']), $k, $prvkey);
394                 openssl_private_decrypt(Strings::base64UrlDecode($data['iv']), $i, $prvkey);
395
396                 return self::decryptAES256CBC(Strings::base64UrlDecode($data['data']), $k, $i);
397         }
398
399
400         /**
401          * Creates cryptographic secure random digits
402          *
403          * @param string $digits The count of digits
404          * @return int The random Digits
405          *
406          * @throws \Exception In case 'random_int' isn't usable
407          */
408         public static function randomDigits($digits)
409         {
410                 $rn = '';
411
412                 // generating cryptographically secure pseudo-random integers
413                 for ($i = 0; $i < $digits; $i++) {
414                         $rn .= random_int(0, 9);
415                 }
416
417                 return $rn;
418         }
419 }