3 namespace Defuse\Crypto;
5 use Defuse\Crypto\Exception as Ex;
10 * Encrypts the input file, saving the ciphertext to the output file.
12 * @param string $inputFilename
13 * @param string $outputFilename
17 * @throws Ex\EnvironmentIsBrokenException
18 * @throws Ex\IOException
20 public static function encryptFile($inputFilename, $outputFilename, Key $key)
22 self::encryptFileInternal(
25 KeyOrPassword::createFromKey($key)
30 * Encrypts a file with a password, using a slow key derivation function to
31 * make password cracking more expensive.
33 * @param string $inputFilename
34 * @param string $outputFilename
35 * @param string $password
38 * @throws Ex\EnvironmentIsBrokenException
39 * @throws Ex\IOException
41 public static function encryptFileWithPassword($inputFilename, $outputFilename, $password)
43 self::encryptFileInternal(
46 KeyOrPassword::createFromPassword($password)
51 * Decrypts the input file, saving the plaintext to the output file.
53 * @param string $inputFilename
54 * @param string $outputFilename
58 * @throws Ex\EnvironmentIsBrokenException
59 * @throws Ex\IOException
60 * @throws Ex\WrongKeyOrModifiedCiphertextException
62 public static function decryptFile($inputFilename, $outputFilename, Key $key)
64 self::decryptFileInternal(
67 KeyOrPassword::createFromKey($key)
72 * Decrypts a file with a password, using a slow key derivation function to
73 * make password cracking more expensive.
75 * @param string $inputFilename
76 * @param string $outputFilename
77 * @param string $password
80 * @throws Ex\EnvironmentIsBrokenException
81 * @throws Ex\IOException
82 * @throws Ex\WrongKeyOrModifiedCiphertextException
84 public static function decryptFileWithPassword($inputFilename, $outputFilename, $password)
86 self::decryptFileInternal(
89 KeyOrPassword::createFromPassword($password)
94 * Takes two resource handles and encrypts the contents of the first,
95 * writing the ciphertext into the second.
97 * @param resource $inputHandle
98 * @param resource $outputHandle
102 * @throws Ex\EnvironmentIsBrokenException
103 * @throws Ex\WrongKeyOrModifiedCiphertextException
105 public static function encryptResource($inputHandle, $outputHandle, Key $key)
107 self::encryptResourceInternal(
110 KeyOrPassword::createFromKey($key)
115 * Encrypts the contents of one resource handle into another with a
116 * password, using a slow key derivation function to make password cracking
119 * @param resource $inputHandle
120 * @param resource $outputHandle
121 * @param string $password
124 * @throws Ex\EnvironmentIsBrokenException
125 * @throws Ex\IOException
126 * @throws Ex\WrongKeyOrModifiedCiphertextException
128 public static function encryptResourceWithPassword($inputHandle, $outputHandle, $password)
130 self::encryptResourceInternal(
133 KeyOrPassword::createFromPassword($password)
138 * Takes two resource handles and decrypts the contents of the first,
139 * writing the plaintext into the second.
141 * @param resource $inputHandle
142 * @param resource $outputHandle
146 * @throws Ex\EnvironmentIsBrokenException
147 * @throws Ex\IOException
148 * @throws Ex\WrongKeyOrModifiedCiphertextException
150 public static function decryptResource($inputHandle, $outputHandle, Key $key)
152 self::decryptResourceInternal(
155 KeyOrPassword::createFromKey($key)
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.
163 * @param resource $inputHandle
164 * @param resource $outputHandle
165 * @param string $password
168 * @throws Ex\EnvironmentIsBrokenException
169 * @throws Ex\IOException
170 * @throws Ex\WrongKeyOrModifiedCiphertextException
172 public static function decryptResourceWithPassword($inputHandle, $outputHandle, $password)
174 self::decryptResourceInternal(
177 KeyOrPassword::createFromPassword($password)
182 * Encrypts a file with either a key or a password.
184 * @param string $inputFilename
185 * @param string $outputFilename
186 * @param KeyOrPassword $secret
189 * @throws Ex\CryptoException
190 * @throws Ex\IOException
192 private static function encryptFileInternal($inputFilename, $outputFilename, KeyOrPassword $secret)
194 /* Open the input file. */
195 $if = @\fopen($inputFilename, 'rb');
197 throw new Ex\IOException(
198 'Cannot open input file for encrypting: ' .
199 self::getLastErrorMessage()
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);
207 /* Open the output file. */
208 $of = @\fopen($outputFilename, 'wb');
211 throw new Ex\IOException(
212 'Cannot open output file for encrypting: ' .
213 self::getLastErrorMessage()
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);
221 /* Perform the encryption. */
223 self::encryptResourceInternal($if, $of, $secret);
224 } catch (Ex\CryptoException $ex) {
230 /* Close the input file. */
231 if (\fclose($if) === false) {
233 throw new Ex\IOException(
234 'Cannot close input file after encrypting'
238 /* Close the output file. */
239 if (\fclose($of) === false) {
240 throw new Ex\IOException(
241 'Cannot close output file after encrypting'
247 * Decrypts a file with either a key or a password.
249 * @param string $inputFilename
250 * @param string $outputFilename
251 * @param KeyOrPassword $secret
254 * @throws Ex\CryptoException
255 * @throws Ex\IOException
257 private static function decryptFileInternal($inputFilename, $outputFilename, KeyOrPassword $secret)
259 /* Open the input file. */
260 $if = @\fopen($inputFilename, 'rb');
262 throw new Ex\IOException(
263 'Cannot open input file for decrypting: ' .
264 self::getLastErrorMessage()
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);
273 /* Open the output file. */
274 $of = @\fopen($outputFilename, 'wb');
277 throw new Ex\IOException(
278 'Cannot open output file for decrypting: ' .
279 self::getLastErrorMessage()
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);
288 /* Perform the decryption. */
290 self::decryptResourceInternal($if, $of, $secret);
291 } catch (Ex\CryptoException $ex) {
297 /* Close the input file. */
298 if (\fclose($if) === false) {
300 throw new Ex\IOException(
301 'Cannot close input file after decrypting'
305 /* Close the output file. */
306 if (\fclose($of) === false) {
307 throw new Ex\IOException(
308 'Cannot close output file after decrypting'
314 * Encrypts a resource with either a key or a password.
316 * @param resource $inputHandle
317 * @param resource $outputHandle
318 * @param KeyOrPassword $secret
321 * @throws Ex\EnvironmentIsBrokenException
322 * @throws Ex\IOException
324 private static function encryptResourceInternal($inputHandle, $outputHandle, KeyOrPassword $secret)
326 if (! \is_resource($inputHandle)) {
327 throw new Ex\IOException(
328 'Input handle must be a resource!'
331 if (! \is_resource($outputHandle)) {
332 throw new Ex\IOException(
333 'Output handle must be a resource!'
337 $inputStat = \fstat($inputHandle);
338 $inputSize = $inputStat['size'];
340 $file_salt = Core::secureRandom(Core::SALT_BYTE_SIZE);
341 $keys = $secret->deriveKeys($file_salt);
342 $ekey = $keys->getEncryptionKey();
343 $akey = $keys->getAuthenticationKey();
345 $ivsize = Core::BLOCK_BYTE_SIZE;
346 $iv = Core::secureRandom($ivsize);
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'
357 /* Write the header, salt, and IV. */
360 Core::CURRENT_VERSION . $file_salt . $iv,
361 Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + $ivsize
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);
369 /* $thisIv will be incremented after each call to the encryption. */
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);
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. */
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'
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. */
389 $read = self::readBytes(
394 $read = self::readBytes(
396 Core::BUFFER_BYTE_SIZE
400 /* Encrypt this buffer. */
402 $encrypted = \openssl_encrypt(
410 if (!\is_string($encrypted)) {
411 throw new Ex\EnvironmentIsBrokenException(
412 'OpenSSL encryption error'
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);
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. */
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);
434 * Decrypts a file-backed resource with either a key or a password.
436 * @param resource $inputHandle
437 * @param resource $outputHandle
438 * @param KeyOrPassword $secret
441 * @throws Ex\EnvironmentIsBrokenException
442 * @throws Ex\IOException
443 * @throws Ex\WrongKeyOrModifiedCiphertextException
445 public static function decryptResourceInternal($inputHandle, $outputHandle, KeyOrPassword $secret)
447 if (! \is_resource($inputHandle)) {
448 throw new Ex\IOException(
449 'Input handle must be a resource!'
452 if (! \is_resource($outputHandle)) {
453 throw new Ex\IOException(
454 'Output handle must be a resource!'
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.'
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.'
475 $file_salt = self::readBytes($inputHandle, Core::SALT_BYTE_SIZE);
478 $ivsize = Core::BLOCK_BYTE_SIZE;
479 $iv = self::readBytes($inputHandle, $ivsize);
481 /* Derive the authentication and encryption keys. */
482 $keys = $secret->deriveKeys($file_salt);
483 $ekey = $keys->getEncryptionKey();
484 $akey = $keys->getAuthenticationKey();
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. */
490 /* $thisIv will be incremented after each call to the decryption. */
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);
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'
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'
511 /* We have the position of the first byte of the HMAC. Go back by one. */
515 /** @var string $stored_mac */
516 $stored_mac = self::readBytes($inputHandle, Core::MAC_BYTE_SIZE);
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'
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'
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'
541 /* PASS #1: Calculating the HMAC. */
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);
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'
559 /* Read the next buffer-sized chunk (or less). */
560 if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) {
562 $read = self::readBytes(
564 $cipher_end - $pos + 1
567 $read = self::readBytes(
569 Core::BUFFER_BYTE_SIZE
573 /* Update the HMAC. */
574 \hash_update($hmac, $read);
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'
584 $macs []= \hash_final($chunk_mac);
587 /* Get the final HMAC, which should match the stored one. */
588 /** @var string $final_mac */
589 $final_mac = \hash_final($hmac, true);
591 /* Verify the HMAC. */
592 if (! Core::hashEquals($final_mac, $stored_mac)) {
593 throw new Ex\WrongKeyOrModifiedCiphertextException(
594 'Integrity check failed.'
598 /* PASS #2: Decrypt and write output. */
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'
607 $at_file_end = false;
608 while (! $at_file_end) {
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'
617 /* Read the next buffer-sized chunk (or less). */
618 if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) {
620 $read = self::readBytes(
622 $cipher_end - $pos + 1
625 $read = self::readBytes(
627 Core::BUFFER_BYTE_SIZE
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'
642 $calc = \hash_final($calc_mac);
645 throw new Ex\WrongKeyOrModifiedCiphertextException(
646 'File was modified after MAC verification'
648 } elseif (! Core::hashEquals(\array_shift($macs), $calc)) {
649 throw new Ex\WrongKeyOrModifiedCiphertextException(
650 'File was modified after MAC verification'
654 /* Decrypt this buffer-sized chunk. */
655 /** @var string $decrypted */
656 $decrypted = \openssl_decrypt(
663 if (!\is_string($decrypted)) {
664 throw new Ex\EnvironmentIsBrokenException(
665 'OpenSSL decryption error'
669 /* Write the plaintext to the output file. */
673 Core::ourStrlen($decrypted)
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. */
686 * Read from a stream; prevent partial reads.
688 * @param resource $stream
689 * @param int $num_bytes
692 * @throws Ex\IOException
693 * @throws Ex\EnvironmentIsBrokenException
697 public static function readBytes($stream, $num_bytes)
699 if ($num_bytes < 0) {
700 throw new Ex\EnvironmentIsBrokenException(
701 'Tried to read less than 0 bytes'
703 } elseif ($num_bytes === 0) {
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'
717 $remaining -= Core::ourStrlen($read);
719 if (Core::ourStrlen($buf) !== $num_bytes) {
720 throw new Ex\IOException(
721 'Tried to read past the end of the file'
728 * Write to a stream; prevents partial writes.
730 * @param resource $stream
732 * @param int $num_bytes
735 * @throws Ex\IOException
739 public static function writeBytes($stream, $buf, $num_bytes = null)
741 $bufSize = Core::ourStrlen($buf);
742 if ($num_bytes === null) {
743 $num_bytes = $bufSize;
745 if ($num_bytes > $bufSize) {
746 throw new Ex\IOException(
747 'Trying to write more bytes than the buffer contains.'
750 if ($num_bytes < 0) {
751 throw new Ex\IOException(
752 'Tried to write less than 0 bytes'
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'
764 $buf = (string) Core::ourSubstr($buf, $written, null);
765 $remaining -= $written;
771 * Returns the last PHP error's or warning's message string.
775 private static function getLastErrorMessage()
777 $error = error_get_last();
778 if ($error === null) {
779 return '[no PHP error]';
781 return $error['message'];