]> git.mxchange.org Git - friendica.git/blob - vendor/defuse/php-encryption/src/File.php
Add defuse/php-encryption 2.0 to Composer dependencies
[friendica.git] / vendor / defuse / php-encryption / src / File.php
1 <?php
2
3 namespace Defuse\Crypto;
4
5 use Defuse\Crypto\Exception as Ex;
6
7 final class File
8 {
9     /**
10      * Encrypts the input file, saving the ciphertext to the output file.
11      *
12      * @param string $inputFilename
13      * @param string $outputFilename
14      * @param Key    $key
15      * @return void
16      *
17      * @throws Ex\EnvironmentIsBrokenException
18      * @throws Ex\IOException
19      */
20     public static function encryptFile($inputFilename, $outputFilename, Key $key)
21     {
22         self::encryptFileInternal(
23             $inputFilename,
24             $outputFilename,
25             KeyOrPassword::createFromKey($key)
26         );
27     }
28
29     /**
30      * Encrypts a file with a password, using a slow key derivation function to
31      * make password cracking more expensive.
32      *
33      * @param string $inputFilename
34      * @param string $outputFilename
35      * @param string $password
36      * @return void
37      *
38      * @throws Ex\EnvironmentIsBrokenException
39      * @throws Ex\IOException
40      */
41     public static function encryptFileWithPassword($inputFilename, $outputFilename, $password)
42     {
43         self::encryptFileInternal(
44             $inputFilename,
45             $outputFilename,
46             KeyOrPassword::createFromPassword($password)
47         );
48     }
49
50     /**
51      * Decrypts the input file, saving the plaintext to the output file.
52      *
53      * @param string $inputFilename
54      * @param string $outputFilename
55      * @param Key    $key
56      * @return void
57      *
58      * @throws Ex\EnvironmentIsBrokenException
59      * @throws Ex\IOException
60      * @throws Ex\WrongKeyOrModifiedCiphertextException
61      */
62     public static function decryptFile($inputFilename, $outputFilename, Key $key)
63     {
64         self::decryptFileInternal(
65             $inputFilename,
66             $outputFilename,
67             KeyOrPassword::createFromKey($key)
68         );
69     }
70
71     /**
72      * Decrypts a file with a password, using a slow key derivation function to
73      * make password cracking more expensive.
74      *
75      * @param string $inputFilename
76      * @param string $outputFilename
77      * @param string $password
78      * @return void
79      *
80      * @throws Ex\EnvironmentIsBrokenException
81      * @throws Ex\IOException
82      * @throws Ex\WrongKeyOrModifiedCiphertextException
83      */
84     public static function decryptFileWithPassword($inputFilename, $outputFilename, $password)
85     {
86         self::decryptFileInternal(
87             $inputFilename,
88             $outputFilename,
89             KeyOrPassword::createFromPassword($password)
90         );
91     }
92
93     /**
94      * Takes two resource handles and encrypts the contents of the first,
95      * writing the ciphertext into the second.
96      *
97      * @param resource $inputHandle
98      * @param resource $outputHandle
99      * @param Key      $key
100      * @return void
101      *
102      * @throws Ex\EnvironmentIsBrokenException
103      * @throws Ex\WrongKeyOrModifiedCiphertextException
104      */
105     public static function encryptResource($inputHandle, $outputHandle, Key $key)
106     {
107         self::encryptResourceInternal(
108             $inputHandle,
109             $outputHandle,
110             KeyOrPassword::createFromKey($key)
111         );
112     }
113
114     /**
115      * Encrypts the contents of one resource handle into another with a
116      * password, using a slow key derivation function to make password cracking
117      * more expensive.
118      *
119      * @param resource $inputHandle
120      * @param resource $outputHandle
121      * @param string   $password
122      * @return void
123      *
124      * @throws Ex\EnvironmentIsBrokenException
125      * @throws Ex\IOException
126      * @throws Ex\WrongKeyOrModifiedCiphertextException
127      */
128     public static function encryptResourceWithPassword($inputHandle, $outputHandle, $password)
129     {
130         self::encryptResourceInternal(
131             $inputHandle,
132             $outputHandle,
133             KeyOrPassword::createFromPassword($password)
134         );
135     }
136
137     /**
138      * Takes two resource handles and decrypts the contents of the first,
139      * writing the plaintext into the second.
140      *
141      * @param resource $inputHandle
142      * @param resource $outputHandle
143      * @param Key      $key
144      * @return void
145      *
146      * @throws Ex\EnvironmentIsBrokenException
147      * @throws Ex\IOException
148      * @throws Ex\WrongKeyOrModifiedCiphertextException
149      */
150     public static function decryptResource($inputHandle, $outputHandle, Key $key)
151     {
152         self::decryptResourceInternal(
153             $inputHandle,
154             $outputHandle,
155             KeyOrPassword::createFromKey($key)
156         );
157     }
158
159     /**
160      * Decrypts the contents of one resource into another with a password, using
161      * a slow key derivation function to make password cracking more expensive.
162      *
163      * @param resource $inputHandle
164      * @param resource $outputHandle
165      * @param string   $password
166      * @return void
167      *
168      * @throws Ex\EnvironmentIsBrokenException
169      * @throws Ex\IOException
170      * @throws Ex\WrongKeyOrModifiedCiphertextException
171      */
172     public static function decryptResourceWithPassword($inputHandle, $outputHandle, $password)
173     {
174         self::decryptResourceInternal(
175             $inputHandle,
176             $outputHandle,
177             KeyOrPassword::createFromPassword($password)
178         );
179     }
180
181     /**
182      * Encrypts a file with either a key or a password.
183      *
184      * @param string        $inputFilename
185      * @param string        $outputFilename
186      * @param KeyOrPassword $secret
187      * @return void
188      *
189      * @throws Ex\CryptoException
190      * @throws Ex\IOException
191      */
192     private static function encryptFileInternal($inputFilename, $outputFilename, KeyOrPassword $secret)
193     {
194         /* Open the input file. */
195         $if = @\fopen($inputFilename, 'rb');
196         if ($if === false) {
197             throw new Ex\IOException(
198                 'Cannot open input file for encrypting: ' .
199                 self::getLastErrorMessage()
200             );
201         }
202         if (\is_callable('\\stream_set_read_buffer')) {
203             /* This call can fail, but the only consequence is performance. */
204             \stream_set_read_buffer($if, 0);
205         }
206
207         /* Open the output file. */
208         $of = @\fopen($outputFilename, 'wb');
209         if ($of === false) {
210             \fclose($if);
211             throw new Ex\IOException(
212                 'Cannot open output file for encrypting: ' .
213                 self::getLastErrorMessage()
214             );
215         }
216         if (\is_callable('\\stream_set_write_buffer')) {
217             /* This call can fail, but the only consequence is performance. */
218             \stream_set_write_buffer($of, 0);
219         }
220
221         /* Perform the encryption. */
222         try {
223             self::encryptResourceInternal($if, $of, $secret);
224         } catch (Ex\CryptoException $ex) {
225             \fclose($if);
226             \fclose($of);
227             throw $ex;
228         }
229
230         /* Close the input file. */
231         if (\fclose($if) === false) {
232             \fclose($of);
233             throw new Ex\IOException(
234                 'Cannot close input file after encrypting'
235             );
236         }
237
238         /* Close the output file. */
239         if (\fclose($of) === false) {
240             throw new Ex\IOException(
241                 'Cannot close output file after encrypting'
242             );
243         }
244     }
245
246     /**
247      * Decrypts a file with either a key or a password.
248      *
249      * @param string        $inputFilename
250      * @param string        $outputFilename
251      * @param KeyOrPassword $secret
252      * @return void
253      *
254      * @throws Ex\CryptoException
255      * @throws Ex\IOException
256      */
257     private static function decryptFileInternal($inputFilename, $outputFilename, KeyOrPassword $secret)
258     {
259         /* Open the input file. */
260         $if = @\fopen($inputFilename, 'rb');
261         if ($if === false) {
262             throw new Ex\IOException(
263                 'Cannot open input file for decrypting: ' .
264                 self::getLastErrorMessage()
265             );
266         }
267
268         if (\is_callable('\\stream_set_read_buffer')) {
269             /* This call can fail, but the only consequence is performance. */
270             \stream_set_read_buffer($if, 0);
271         }
272
273         /* Open the output file. */
274         $of = @\fopen($outputFilename, 'wb');
275         if ($of === false) {
276             \fclose($if);
277             throw new Ex\IOException(
278                 'Cannot open output file for decrypting: ' .
279                 self::getLastErrorMessage()
280             );
281         }
282
283         if (\is_callable('\\stream_set_write_buffer')) {
284             /* This call can fail, but the only consequence is performance. */
285             \stream_set_write_buffer($of, 0);
286         }
287
288         /* Perform the decryption. */
289         try {
290             self::decryptResourceInternal($if, $of, $secret);
291         } catch (Ex\CryptoException $ex) {
292             \fclose($if);
293             \fclose($of);
294             throw $ex;
295         }
296
297         /* Close the input file. */
298         if (\fclose($if) === false) {
299             \fclose($of);
300             throw new Ex\IOException(
301                 'Cannot close input file after decrypting'
302             );
303         }
304
305         /* Close the output file. */
306         if (\fclose($of) === false) {
307             throw new Ex\IOException(
308                 'Cannot close output file after decrypting'
309             );
310         }
311     }
312
313     /**
314      * Encrypts a resource with either a key or a password.
315      *
316      * @param resource      $inputHandle
317      * @param resource      $outputHandle
318      * @param KeyOrPassword $secret
319      * @return void
320      *
321      * @throws Ex\EnvironmentIsBrokenException
322      * @throws Ex\IOException
323      */
324     private static function encryptResourceInternal($inputHandle, $outputHandle, KeyOrPassword $secret)
325     {
326         if (! \is_resource($inputHandle)) {
327             throw new Ex\IOException(
328                 'Input handle must be a resource!'
329             );
330         }
331         if (! \is_resource($outputHandle)) {
332             throw new Ex\IOException(
333                 'Output handle must be a resource!'
334             );
335         }
336
337         $inputStat = \fstat($inputHandle);
338         $inputSize = $inputStat['size'];
339
340         $file_salt = Core::secureRandom(Core::SALT_BYTE_SIZE);
341         $keys = $secret->deriveKeys($file_salt);
342         $ekey = $keys->getEncryptionKey();
343         $akey = $keys->getAuthenticationKey();
344
345         $ivsize = Core::BLOCK_BYTE_SIZE;
346         $iv     = Core::secureRandom($ivsize);
347
348         /* Initialize a streaming HMAC state. */
349         /** @var resource $hmac */
350         $hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey);
351         if (!\is_resource($hmac)) {
352             throw new Ex\EnvironmentIsBrokenException(
353                 'Cannot initialize a hash context'
354             );
355         }
356
357         /* Write the header, salt, and IV. */
358         self::writeBytes(
359             $outputHandle,
360             Core::CURRENT_VERSION . $file_salt . $iv,
361             Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + $ivsize
362         );
363
364         /* Add the header, salt, and IV to the HMAC. */
365         \hash_update($hmac, Core::CURRENT_VERSION);
366         \hash_update($hmac, $file_salt);
367         \hash_update($hmac, $iv);
368
369         /* $thisIv will be incremented after each call to the encryption. */
370         $thisIv = $iv;
371
372         /* How many blocks do we encrypt at a time? We increment by this value. */
373         $inc = (int) (Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE);
374
375         /* Loop until we reach the end of the input file. */
376         $at_file_end = false;
377         while (! (\feof($inputHandle) || $at_file_end)) {
378             /* Find out if we can read a full buffer, or only a partial one. */
379             /** @var int */
380             $pos = \ftell($inputHandle);
381             if (!\is_int($pos)) {
382                 throw new Ex\IOException(
383                     'Could not get current position in input file during encryption'
384                 );
385             }
386             if ($pos + Core::BUFFER_BYTE_SIZE >= $inputSize) {
387                 /* We're at the end of the file, so we need to break out of the loop. */
388                 $at_file_end = true;
389                 $read = self::readBytes(
390                     $inputHandle,
391                     $inputSize - $pos
392                 );
393             } else {
394                 $read = self::readBytes(
395                     $inputHandle,
396                     Core::BUFFER_BYTE_SIZE
397                 );
398             }
399
400             /* Encrypt this buffer. */
401             /** @var string */
402             $encrypted = \openssl_encrypt(
403                 $read,
404                 Core::CIPHER_METHOD,
405                 $ekey,
406                 OPENSSL_RAW_DATA,
407                 $thisIv
408             );
409
410             if (!\is_string($encrypted)) {
411                 throw new Ex\EnvironmentIsBrokenException(
412                     'OpenSSL encryption error'
413                 );
414             }
415
416             /* Write this buffer's ciphertext. */
417             self::writeBytes($outputHandle, $encrypted, Core::ourStrlen($encrypted));
418             /* Add this buffer's ciphertext to the HMAC. */
419             \hash_update($hmac, $encrypted);
420
421             /* Increment the counter by the number of blocks in a buffer. */
422             $thisIv = Core::incrementCounter($thisIv, $inc);
423             /* WARNING: Usually, unless the file is a multiple of the buffer
424              * size, $thisIv will contain an incorrect value here on the last
425              * iteration of this loop. */
426         }
427
428         /* Get the HMAC and append it to the ciphertext. */
429         $final_mac = \hash_final($hmac, true);
430         self::writeBytes($outputHandle, $final_mac, Core::MAC_BYTE_SIZE);
431     }
432
433     /**
434      * Decrypts a file-backed resource with either a key or a password.
435      *
436      * @param resource      $inputHandle
437      * @param resource      $outputHandle
438      * @param KeyOrPassword $secret
439      * @return void
440      *
441      * @throws Ex\EnvironmentIsBrokenException
442      * @throws Ex\IOException
443      * @throws Ex\WrongKeyOrModifiedCiphertextException
444      */
445     public static function decryptResourceInternal($inputHandle, $outputHandle, KeyOrPassword $secret)
446     {
447         if (! \is_resource($inputHandle)) {
448             throw new Ex\IOException(
449                 'Input handle must be a resource!'
450             );
451         }
452         if (! \is_resource($outputHandle)) {
453             throw new Ex\IOException(
454                 'Output handle must be a resource!'
455             );
456         }
457
458         /* Make sure the file is big enough for all the reads we need to do. */
459         $stat = \fstat($inputHandle);
460         if ($stat['size'] < Core::MINIMUM_CIPHERTEXT_SIZE) {
461             throw new Ex\WrongKeyOrModifiedCiphertextException(
462                 'Input file is too small to have been created by this library.'
463             );
464         }
465
466         /* Check the version header. */
467         $header = self::readBytes($inputHandle, Core::HEADER_VERSION_SIZE);
468         if ($header !== Core::CURRENT_VERSION) {
469             throw new Ex\WrongKeyOrModifiedCiphertextException(
470                 'Bad version header.'
471             );
472         }
473
474         /* Get the salt. */
475         $file_salt = self::readBytes($inputHandle, Core::SALT_BYTE_SIZE);
476
477         /* Get the IV. */
478         $ivsize = Core::BLOCK_BYTE_SIZE;
479         $iv     = self::readBytes($inputHandle, $ivsize);
480
481         /* Derive the authentication and encryption keys. */
482         $keys = $secret->deriveKeys($file_salt);
483         $ekey = $keys->getEncryptionKey();
484         $akey = $keys->getAuthenticationKey();
485
486         /* We'll store the MAC of each buffer-sized chunk as we verify the
487          * actual MAC, so that we can check them again when decrypting. */
488         $macs = [];
489
490         /* $thisIv will be incremented after each call to the decryption. */
491         $thisIv = $iv;
492
493         /* How many blocks do we encrypt at a time? We increment by this value. */
494         $inc = (int) (Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE);
495
496         /* Get the HMAC. */
497         if (\fseek($inputHandle, (-1 * Core::MAC_BYTE_SIZE), SEEK_END) === false) {
498             throw new Ex\IOException(
499                 'Cannot seek to beginning of MAC within input file'
500             );
501         }
502
503         /* Get the position of the last byte in the actual ciphertext. */
504         /** @var int $cipher_end */
505         $cipher_end = \ftell($inputHandle);
506         if (!\is_int($cipher_end)) {
507             throw new Ex\IOException(
508                 'Cannot read input file'
509             );
510         }
511         /* We have the position of the first byte of the HMAC. Go back by one. */
512         --$cipher_end;
513
514         /* Read the HMAC. */
515         /** @var string $stored_mac */
516         $stored_mac = self::readBytes($inputHandle, Core::MAC_BYTE_SIZE);
517
518         /* Initialize a streaming HMAC state. */
519         /** @var resource $hmac */
520         $hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey);
521         if (!\is_resource($hmac)) {
522             throw new Ex\EnvironmentIsBrokenException(
523                 'Cannot initialize a hash context'
524             );
525         }
526
527         /* Reset file pointer to the beginning of the file after the header */
528         if (\fseek($inputHandle, Core::HEADER_VERSION_SIZE, SEEK_SET) === false) {
529             throw new Ex\IOException(
530                 'Cannot read seek within input file'
531             );
532         }
533
534         /* Seek to the start of the actual ciphertext. */
535         if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize, SEEK_CUR) === false) {
536             throw new Ex\IOException(
537                 'Cannot seek input file to beginning of ciphertext'
538             );
539         }
540
541         /* PASS #1: Calculating the HMAC. */
542
543         \hash_update($hmac, $header);
544         \hash_update($hmac, $file_salt);
545         \hash_update($hmac, $iv);
546         /** @var resource $hmac2 */
547         $hmac2 = \hash_copy($hmac);
548
549         $break = false;
550         while (! $break) {
551             /** @var int $pos */
552             $pos = \ftell($inputHandle);
553             if (!\is_int($pos)) {
554                 throw new Ex\IOException(
555                     'Could not get current position in input file during decryption'
556                 );
557             }
558
559             /* Read the next buffer-sized chunk (or less). */
560             if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) {
561                 $break = true;
562                 $read  = self::readBytes(
563                     $inputHandle,
564                     $cipher_end - $pos + 1
565                 );
566             } else {
567                 $read = self::readBytes(
568                     $inputHandle,
569                     Core::BUFFER_BYTE_SIZE
570                 );
571             }
572
573             /* Update the HMAC. */
574             \hash_update($hmac, $read);
575
576             /* Remember this buffer-sized chunk's HMAC. */
577             /** @var resource $chunk_mac */
578             $chunk_mac = \hash_copy($hmac);
579             if (!\is_resource($chunk_mac)) {
580                 throw new Ex\EnvironmentIsBrokenException(
581                     'Cannot duplicate a hash context'
582                 );
583             }
584             $macs []= \hash_final($chunk_mac);
585         }
586
587         /* Get the final HMAC, which should match the stored one. */
588         /** @var string $final_mac */
589         $final_mac = \hash_final($hmac, true);
590
591         /* Verify the HMAC. */
592         if (! Core::hashEquals($final_mac, $stored_mac)) {
593             throw new Ex\WrongKeyOrModifiedCiphertextException(
594                 'Integrity check failed.'
595             );
596         }
597
598         /* PASS #2: Decrypt and write output. */
599
600         /* Rewind to the start of the actual ciphertext. */
601         if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize + Core::HEADER_VERSION_SIZE, SEEK_SET) === false) {
602             throw new Ex\IOException(
603                 'Could not move the input file pointer during decryption'
604             );
605         }
606
607         $at_file_end = false;
608         while (! $at_file_end) {
609             /** @var int $pos */
610             $pos = \ftell($inputHandle);
611             if (!\is_int($pos)) {
612                 throw new Ex\IOException(
613                     'Could not get current position in input file during decryption'
614                 );
615             }
616
617             /* Read the next buffer-sized chunk (or less). */
618             if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) {
619                 $at_file_end = true;
620                 $read   = self::readBytes(
621                     $inputHandle,
622                     $cipher_end - $pos + 1
623                 );
624             } else {
625                 $read = self::readBytes(
626                     $inputHandle,
627                     Core::BUFFER_BYTE_SIZE
628                 );
629             }
630
631             /* Recalculate the MAC (so far) and compare it with the one we
632              * remembered from pass #1 to ensure attackers didn't change the
633              * ciphertext after MAC verification. */
634             \hash_update($hmac2, $read);
635             /** @var resource $calc_mac */
636             $calc_mac = \hash_copy($hmac2);
637             if (!\is_resource($calc_mac)) {
638                 throw new Ex\EnvironmentIsBrokenException(
639                     'Cannot duplicate a hash context'
640                 );
641             }
642             $calc = \hash_final($calc_mac);
643
644             if (empty($macs)) {
645                 throw new Ex\WrongKeyOrModifiedCiphertextException(
646                     'File was modified after MAC verification'
647                 );
648             } elseif (! Core::hashEquals(\array_shift($macs), $calc)) {
649                 throw new Ex\WrongKeyOrModifiedCiphertextException(
650                     'File was modified after MAC verification'
651                 );
652             }
653
654             /* Decrypt this buffer-sized chunk. */
655             /** @var string $decrypted */
656             $decrypted = \openssl_decrypt(
657                 $read,
658                 Core::CIPHER_METHOD,
659                 $ekey,
660                 OPENSSL_RAW_DATA,
661                 $thisIv
662             );
663             if (!\is_string($decrypted)) {
664                 throw new Ex\EnvironmentIsBrokenException(
665                     'OpenSSL decryption error'
666                 );
667             }
668
669             /* Write the plaintext to the output file. */
670             self::writeBytes(
671                 $outputHandle,
672                 $decrypted,
673                 Core::ourStrlen($decrypted)
674             );
675
676             /* Increment the IV by the amount of blocks in a buffer. */
677             /** @var string $thisIv */
678             $thisIv = Core::incrementCounter($thisIv, $inc);
679             /* WARNING: Usually, unless the file is a multiple of the buffer
680              * size, $thisIv will contain an incorrect value here on the last
681              * iteration of this loop. */
682         }
683     }
684
685     /**
686      * Read from a stream; prevent partial reads.
687      *
688      * @param resource $stream
689      * @param int      $num_bytes
690      * @return string
691      *
692      * @throws Ex\IOException
693      * @throws Ex\EnvironmentIsBrokenException
694      *
695      * @return string
696      */
697     public static function readBytes($stream, $num_bytes)
698     {
699         if ($num_bytes < 0) {
700             throw new Ex\EnvironmentIsBrokenException(
701                 'Tried to read less than 0 bytes'
702             );
703         } elseif ($num_bytes === 0) {
704             return '';
705         }
706         $buf       = '';
707         $remaining = $num_bytes;
708         while ($remaining > 0 && ! \feof($stream)) {
709             /** @var string $read */
710             $read = \fread($stream, $remaining);
711             if (!\is_string($read)) {
712                 throw new Ex\IOException(
713                     'Could not read from the file'
714                 );
715             }
716             $buf .= $read;
717             $remaining -= Core::ourStrlen($read);
718         }
719         if (Core::ourStrlen($buf) !== $num_bytes) {
720             throw new Ex\IOException(
721                 'Tried to read past the end of the file'
722             );
723         }
724         return $buf;
725     }
726
727     /**
728      * Write to a stream; prevents partial writes.
729      *
730      * @param resource $stream
731      * @param string   $buf
732      * @param int      $num_bytes
733      * @return int
734      *
735      * @throws Ex\IOException
736      *
737      * @return string
738      */
739     public static function writeBytes($stream, $buf, $num_bytes = null)
740     {
741         $bufSize = Core::ourStrlen($buf);
742         if ($num_bytes === null) {
743             $num_bytes = $bufSize;
744         }
745         if ($num_bytes > $bufSize) {
746             throw new Ex\IOException(
747                 'Trying to write more bytes than the buffer contains.'
748             );
749         }
750         if ($num_bytes < 0) {
751             throw new Ex\IOException(
752                 'Tried to write less than 0 bytes'
753             );
754         }
755         $remaining = $num_bytes;
756         while ($remaining > 0) {
757             /** @var int $written */
758             $written = \fwrite($stream, $buf, $remaining);
759             if (!\is_int($written)) {
760                 throw new Ex\IOException(
761                     'Could not write to the file'
762                 );
763             }
764             $buf = (string) Core::ourSubstr($buf, $written, null);
765             $remaining -= $written;
766         }
767         return $num_bytes;
768     }
769
770     /**
771      * Returns the last PHP error's or warning's message string.
772      *
773      * @return string
774      */
775     private static function getLastErrorMessage()
776     {
777         $error = error_get_last();
778         if ($error === null) {
779             return '[no PHP error]';
780         } else {
781             return $error['message'];
782         }
783     }
784 }