File: src/File.php

Recommend this page to a friend!
  Classes of Scott Arciszewski  >  Halite  >  src/File.php  >  Download  
File: src/File.php
Role: Class source
Content type: text/plain
Description: Class source
Class: Halite
Perform cryptography operations with libsodium
Author: By
Last change: Limit the File API.
For version 2, let's use strict types!
Switch passwords and key derivation to crypto_pwhash() (Argon2i)
Merge branch 'v2.0' of https://github.com/paragonie/halite into v2.0

Conflicts:
src/Symmetric/Crypto.php
Date: 5 years ago
Size: 45,473 bytes
 

 

Contents

Class file image Download
<?php
declare(strict_types=1);
namespace ParagonIE\Halite;

use \ParagonIE\Halite\Alerts as CryptoException;
use \ParagonIE\Halite\{
    Asymmetric\Crypto as AsymmetricCrypto,
    Asymmetric\EncryptionPublicKey,
    Asymmetric\EncryptionSecretKey,
    Asymmetric\SignaturePublicKey,
    Asymmetric\SignatureSecretKey,
    Contract\FileInterface,
    Contract\KeyInterface,
    Contract\StreamInterface,
    Stream\MutableFile,
    Stream\ReadOnlyFile,
    Symmetric\AuthenticationKey,
    Symmetric\EncryptionKey
};
use ParagonIE\Halite\Asymmetric\Crypto;

final class File implements FileInterface
{
    /**
     * Lazy fallthrough method for checksumFile() and checksumResource()
     * 
     * @param string|resource $filepath
     * @param AuthenticationKey $key
     * @param bool $raw
     * @return string
     * @throws CryptoException\InvalidType
     */
    public static function checksum(
        $filepath,
        KeyInterface $key = null,
        $raw = false
    ): string {
        if (\is_resource($filepath) || \is_string($filepath)) {
            return self::checksumStream(
                new ReadOnlyFile($filepath),
                $key,
                $raw
            );
        }
        throw new CryptoException\InvalidType(
            'Argument 1: Expected a filename or resource'
        );
    }
    
    /**
     * Lazy fallthrough method for encryptFile() and encryptResource()
     * 
     * @param string|resource $input
     * @param string|resource $output
     * @param EncryptionKey $key
     * @return int
     * @throws CryptoException\InvalidType
     */
    public static function encrypt(
        $input,
        $output,
        EncryptionKey $key
    ): int {
        if (
            \is_resource($input) ||
            \is_resource($output) ||
            \is_string($input) ||
            \is_string($output)
        ) {
            return self::encryptStream(
                new ReadOnlyFile($input),
                new MutableFile($output),
                $key
            );
        }
        throw new CryptoException\InvalidType(
            'Argument 1: Expected a filename or resource'
        );
    }
    
    /**
     * Lazy fallthrough method for decryptFile() and decryptResource()
     * 
     * @param string|resource $input
     * @param string|resource $output
     * @param EncryptionKey $key
     * @return bool
     * @throws CryptoException\InvalidType
     */
    public static function decrypt(
        $input,
        $output,
        EncryptionKey $key
    ): bool {
        if (
            \is_resource($input) ||
            \is_resource($output) ||
            \is_string($input) ||
            \is_string($output)
        ) {
            try {
                $readOnly = new ReadOnlyFile($input);
                $mutable = new MutableFile($output);
                return self::decryptStream(
                    $readOnly,
                    $mutable,
                    $key
                );
            } catch (CryptoException\HaliteAlert $ex) {
                $readOnly->close();
                $mutable->close();
                throw $ex;
            }
        }
        throw new CryptoException\InvalidType(
            'Strings or file handles expected'
        );
    }

    /**
     * Lazy fallthrough method for sealFile() and sealResource()
     *
     * @param string|resource $input
     * @param string|resource $output
     * @param EncryptionPublicKey $publickey
     * @return int Number of bytes written
     * @throws Alerts\InvalidType
     */
    public static function seal(
        $input,
        $output,
        EncryptionPublicKey $publickey
    ): int {
        if (
            \is_resource($input) ||
            \is_resource($output) ||
            \is_string($input) ||
            \is_string($output)
        ) {
            return self::sealStream(
                new ReadOnlyFile($input),
                new MutableFile($output),
                $publickey
            );
        }
        throw new CryptoException\InvalidType(
            'Argument 1: Expected a filename or resource'
        );
    }
    
    /**
     * Lazy fallthrough method for sealFile() and sealResource()
     * 
     * @param string|resource $input
     * @param string|resource $output
     * @param EncryptionSecretKey $secretkey
     * @return bool TRUE on success
     * @throws CryptoException\InvalidType
     */
    public static function unseal(
        $input,
        $output,
        EncryptionSecretKey $secretkey
    ): bool {
        if (
            \is_resource($input) ||
            \is_resource($output) ||
            \is_string($input) ||
            \is_string($output)
        ) {
            try {
                $readOnly = new ReadOnlyFile($input);
                $mutable = new MutableFile($output);
                return self::unsealStream(
                    $readOnly,
                    $mutable,
                    $secretkey
                );
            } catch (CryptoException\HaliteAlert $ex) {
                $readOnly->close();
                $mutable->close();
                throw $ex;
            }
        }
        throw new CryptoException\InvalidType(
            'Argument 1: Expected a filename or resource'
        );
    }
    
    /**
     * Lazy fallthrough method for signFile() and signResource()
     * 
     * @param string|resource $filename
     * @param SignatureSecretKey $secretkey
     * @param bool $raw_binary
     * @return string
     * @throws Alerts\InvalidType
     */
    public static function sign(
        $filename,
        SignatureSecretKey $secretkey,
        bool $raw_binary = false
    ): string {
        if (
            \is_resource($filename) ||
            \is_string($filename)
        ) {
            return self::signStream(
                new ReadOnlyFile($filename),
                $secretkey,
                $raw_binary
            );
        }
        throw new CryptoException\InvalidType(
            'Argument 1: Expected a filename or resource'
        );
    }
    
    /**
     * Lazy fallthrough method for verifyFile() and verifyResource()
     * 
     * @param string|resource $filename
     * @param SignaturePublicKey $publickey
     * @param string $signature
     * @param bool $raw_binary
     * 
     * @return string
     * @throws Alerts\InvalidType
     */
    public static function verify(
        $filename,
        SignaturePublicKey $publickey,
        string $signature,
        bool $raw_binary = false
    ): bool {
        if (
            \is_resource($filename) ||
            \is_string($filename)
        ) {
            return self::verifyStream(
                new ReadOnlyFile($filename),
                $publickey,
                $signature,
                $raw_binary
            );
        }
        throw new CryptoException\InvalidType(
            'Argument 1: Expected a filename or resource'
        );
    }

    /**
     * Calculate a checksum (derived from BLAKE2b) of a file, by filename
     *
     * @param string $filepath The file you'd like to checksum
     * @param Key $key An optional key to use in the BLAKE2b hash
     * @param bool $raw Set to true if you don't want hex
     * @return string
     * @throws Alerts\FileAccessDenied
     */
    protected static function checksumFile(
        string $filepath,
        Key $key = null,
        bool $raw = false
    ): string {
        if (!is_readable($filepath)) {
            throw new CryptoException\FileAccessDenied(
                'Could not read the file'
            );
        }
        $fp = \fopen($filepath, 'rb');
        if ($fp === false) {
            throw new CryptoException\FileAccessDenied(
                'Could not read the file'
            );
        }
        try {
            $checksum = self::checksumResource($fp, $key, $raw);
        } catch (CryptoException\HaliteAlert $e) {
            \fclose($fp);
            throw $e;
        }
        \fclose($fp);
        return $checksum;
    }
    
    
    /**
     * Calculate a checksum (derived from BLAKE2b) of a file handle
     * 
     * @param string $fileHandle The file you'd like to checksum
     * @param Key $key An optional key to use in the BLAKE2b hash
     * @param bool $raw Set to true if you don't want hex
     * 
     * @return string
     * @throws Alerts\InvalidType
     */
    protected static function checksumResource(
        $fileHandle,
        Key $key = null,
        bool $raw = false
    ): string {
        // Input validation
        if (!\is_resource($fileHandle)) {
            throw new CryptoException\InvalidType(
                'Expected input handle to be a resource'
            );
        }
        
        return self::checksumStream(
            new ReadOnlyFile($fileHandle),
            $key,
            $raw
        );
    }
    /**
     * Calculate the BLAKE2b checksum of an entire stream
     *
     * @param StreamInterface $fileStream
     * @param Key $key
     * @param bool $raw
     * @return string
     * @throws CryptoException\InvalidKey
     */
    protected static function checksumStream(
        StreamInterface $fileStream,
        Key $key = null,
        bool $raw = false
    ): string {
        $config = self::getConfig(
            Halite::HALITE_VERSION_FILE,
            'checksum'
        );
        if ($key instanceof AuthenticationKey) {
            $state = \Sodium\crypto_generichash_init(
                $key->get(),
                $config->HASH_LEN
            );
        } elseif($config->CHECKSUM_PUBKEY && $key instanceof SignaturePublicKey) {
            // In version 2, we use the public key as a hash key
            $state = \Sodium\crypto_generichash_init(
                $key->get(),
                $config->HASH_LEN
            );
        } elseif (isset($key)) {
            throw new CryptoException\InvalidKey(
                'Argument 2: Expected an instance of AuthenticationKey'
            );
        } else {
            $state = \Sodium\crypto_generichash_init(
                '',
                $config->HASH_LEN
            );
        }
        $size = $fileStream->getSize();
        while ($fileStream->remainingBytes() > 0) {
            $read = $fileStream->readBytes(
                // Don't go past the file size even if $config->BUFFER is not an even multiple of it:
                ($fileStream->getPos() + $config->BUFFER) > $size
                    ? ($size - $fileStream->getPos())
                    : $config->BUFFER
            );
            \Sodium\crypto_generichash_update($state, $read);
        }
        if ($raw) {
            return \Sodium\crypto_generichash_final($state, $config->HASH_LEN);
        }
        return \Sodium\bin2hex(
            \Sodium\crypto_generichash_final($state, $config->HASH_LEN)
        );
    }

    /**
     * Encrypt a file with a symmetric key
     * 
     * @param string $inputFile
     * @param string $outputFile
     * @param EncryptionKey $key
     * @return int
     * @throws CryptoException\FileAccessDenied
     * @throws CryptoException\HaliteAlert
     */
    protected static function encryptFile(
        string $inputFile,
        string $outputFile,
        EncryptionKey $key
    ): int {
        if (!\is_readable($inputFile)) {
            throw new CryptoException\FileAccessDenied(
                'Could not read from the file'
            );
        }
        if (!\is_writable($outputFile)) {
            throw new CryptoException\FileAccessDenied(
                'Could not write to the file'
            );
        }
        $inputHandle = \fopen($inputFile, 'rb');
        if ($inputHandle === false) {
            throw new CryptoException\FileAccessDenied(
                'Could not read from the file'
            );
        }
        $outputHandle = \fopen($outputFile, 'wb');
        if ($outputHandle === false) {
            \fclose($inputHandle);
            throw new CryptoException\FileAccessDenied(
                'Could not write to the file'
            );
        }
        
        try {
            $result = self::encryptResource(
                $inputHandle,
                $outputHandle,
                $key
            );
            \fclose($inputHandle);
            \fclose($outputHandle);
            return $result;
        } catch (CryptoException\HaliteAlert $e) {
            \fclose($inputHandle);
            \fclose($outputHandle);
            
            // Rethrow the exception:
            throw $e;
        }
    }
    
    /**
     * Decrypt a file with a symmetric key
     * 
     * @param string $inputFile
     * @param string $outputFile
     * @param EncryptionKey $key
     * @return bool
     *
     * @throws CryptoException\FileAccessDenied
     * @throws CryptoException\HaliteAlert
     */
    protected static function decryptFile(
        string $inputFile,
        string $outputFile,
        EncryptionKey $key
    ): bool {
        if (!\is_readable($inputFile)) {
            throw new CryptoException\FileAccessDenied(
                'Could not read from the file'
            );
        }
        if (!\is_writable($outputFile)) {
            throw new CryptoException\FileAccessDenied(
                'Could not write to the file'
            );
        }
        $inputHandle = \fopen($inputFile, 'rb');
        if ($inputHandle === false) {
            throw new CryptoException\FileAccessDenied(
                'Could not read from the file'
            );
        }
        $outputHandle = \fopen($outputFile, 'wb');
        if ($outputHandle === false) {
            \fclose($inputHandle);
            throw new CryptoException\FileAccessDenied(
                'Could not write to the file'
            );
        }
        
        try {
            $result = self::decryptResource(
                $inputHandle,
                $outputHandle,
                $key
            );

            \fclose($inputHandle);
            \fclose($outputHandle);
            return $result;
        } catch (CryptoException\HaliteAlert $e) {
            \fclose($inputHandle);
            \fclose($outputHandle);
            
            // Rethrow the exception:
            throw $e;
        }
    }
    
    /**
     * Encrypt a file with a public key
     * 
     * @param string $inputFile
     * @param string $outputFile
     * @param EncryptionPublicKey $publickey
     * @return int
     * @throws CryptoException\FileAccessDenied
     * @throws CryptoException\HaliteAlert
     */
    protected static function sealFile(
        string $inputFile,
        string $outputFile,
        EncryptionPublicKey $publickey
    ): int {
        if (!\is_readable($inputFile)) {
            throw new CryptoException\FileAccessDenied(
                'Could not read from the file'
            );
        }
        if (!\is_writable($outputFile)) {
            throw new CryptoException\FileAccessDenied(
                'Could not write to the file'
            );
        }
        $inputHandle = \fopen($inputFile, 'rb');
        if ($inputHandle === false) {
            throw new CryptoException\FileAccessDenied(
                'Could not read from the file'
            );
        }
        $outputHandle = \fopen($outputFile, 'wb');
        if ($outputHandle === false) {
            \fclose($inputHandle);
            throw new CryptoException\FileAccessDenied(
                'Could not write to the file'
            );
        }
        
        try {
            $return = self::sealResource(
                $inputHandle,
                $outputHandle,
                $publickey
            );

            \fclose($inputHandle);
            \fclose($outputHandle);
            return $return;
        } catch (CryptoException\HaliteAlert $e) {
            \fclose($inputHandle);
            \fclose($outputHandle);
            
            // Rethrow the exception:
            throw $e;
        }
    }
    
    /**
     * Decrypt a file with a private key
     * 
     * @param string $inputFile
     * @param string $outputFile
     * @param EncryptionSecretKey $secretkey
     * @return bool
     * @throws CryptoException\FileAccessDenied
     * @throws CryptoException\HaliteAlert
     */
    protected static function unsealFile(
        string $inputFile,
        string $outputFile,
        EncryptionSecretKey $secretkey
    ): bool {
        if (!\is_readable($inputFile)) {
            throw new CryptoException\FileAccessDenied(
                'Could not read from the file'
            );
        }
        if (!\is_writable($outputFile)) {
            throw new CryptoException\FileAccessDenied(
                'Could not write to the file'
            );
        }
        $inputHandle = \fopen($inputFile, 'rb');
        if ($inputHandle === false) {
            throw new CryptoException\FileAccessDenied(
                'Could not read from the file'
            );
        }
        $outputHandle = \fopen($outputFile, 'wb');
        if ($outputHandle === false) {
            \fclose($inputHandle);
            throw new CryptoException\FileAccessDenied(
                'Could not write to the file'
            );
        }
        
        try {
            $return = self::unsealResource(
                $inputHandle,
                $outputHandle,
                $secretkey
            );
            
            \fclose($inputHandle);
            \fclose($outputHandle);
            return $return;
        } catch (CryptoException\HaliteAlert $e) {
            \fclose($inputHandle);
            \fclose($outputHandle);
            
            // Rethrow the exception:
            throw $e;
        }
    }
    
    /**
     * Signs a file
     * 
     * @param string $filename
     * @param SignatureSecretKey $secretkey
     * @param bool $raw_binary
     * @return string
     * @throws CryptoException\FileAccessDenied
     * @throws CryptoException\HaliteAlert
     */
    protected static function signFile(
        string $filename,
        SignatureSecretKey $secretkey,
        bool $raw_binary = false
    ): string {
        
        if (!\is_readable($filename)) {
            throw new CryptoException\FileAccessDenied(
                'Could not read from the file'
            );
        }
        $inputHandle = \fopen($filename, 'rb');
        if ($inputHandle === false) {
            throw new CryptoException\FileAccessDenied(
                'Could not read from the file'
            );
        }
        
        try {
            $return = self::signResource(
                $inputHandle,
                $secretkey,
                $raw_binary
            );

            \fclose($inputHandle);
            return $return;
        } catch (CryptoException\HaliteAlert $e) {
            \fclose($inputHandle);
            // Rethrow the exception:
            throw $e;
        }
    }
    
    /**
     * Verifies a file
     * 
     * @param string $filename
     * @param SignaturePublicKey $publickey
     * @param string $signature
     * @return bool
     * @throws CryptoException\FileAccessDenied
     * @throws CryptoException\HaliteAlert
     */
    protected static function verifyFile(
        string $filename,
        SignaturePublicKey $publickey,
        string $signature,
        bool $raw_binary = false
    ): bool {
        
        if (!\is_readable($filename)) {
            throw new CryptoException\FileAccessDenied(
                'Could not read from the file'
            );
        }
        $inputHandle = \fopen($filename, 'rb');
        if ($inputHandle === false) {
            throw new CryptoException\FileAccessDenied(
                'Could not read from the file'
            );
        }
        
        try {
            $return = self::verifyResource(
                $inputHandle,
                $publickey,
                $signature,
                $raw_binary
            );

            \fclose($inputHandle);
            return $return;
        } catch (CryptoException\HaliteAlert $e) {
            \fclose($inputHandle);
            // Rethrow the exception:
            throw $e;
        }
    }
    
    /**
     * Encrypt a (file handle)
     * 
     * @param $input
     * @param $output
     * @param EncryptionKey $key
     * @return int
     * @throws CryptoException\InvalidType
     */
    protected static function encryptResource(
        $input,
        $output,
        EncryptionKey $key
    ): int {
        // Input validation
        if (!\is_resource($input)) {
            throw new \ParagonIE\Halite\Alerts\InvalidType(
                'Expected input handle to be a resource'
            );
        }
        if (!\is_resource($output)) {
            throw new \ParagonIE\Halite\Alerts\InvalidType(
                'Expected output handle to be a resource'
            );
        }
        return self::encryptStream(
            new ReadOnlyFile($input),
            new MutableFile($output),
            $key
        );
    }
    
    /**
     * Encrypt a (file handle)
     * 
     * @param $input
     * @param $output
     * @param EncryptionKey $key
     */
    protected static function encryptStream(
        ReadOnlyFile $input,
        MutableFile $output,
        EncryptionKey $key
    ): int {
        $config = self::getConfig(Halite::HALITE_VERSION_FILE, 'encrypt');
        
        // Generate a nonce and HKDF salt
        $firstnonce = \Sodium\randombytes_buf($config->NONCE_BYTES);
        $hkdfsalt = \Sodium\randombytes_buf($config->HKDF_SALT_LEN);
        
        // Let's split our key
        list ($encKey, $authKey) = self::splitKeys($key, $hkdfsalt, $config);
        $mac = \hash_init('sha256', HASH_HMAC, $authKey);
        // We no longer need $authKey after we set up the hash context
        unset($authKey);
        
        // Write the header
        $output->writeBytes(Halite::HALITE_VERSION_FILE, Halite::VERSION_TAG_LEN);
        $output->writeBytes($firstnonce, \Sodium\CRYPTO_STREAM_NONCEBYTES);
        $output->writeBytes($hkdfsalt, $config->HKDF_SALT_LEN);
        
        \hash_update($mac, Halite::HALITE_VERSION_FILE);
        \hash_update($mac, $firstnonce);
        \hash_update($mac, $hkdfsalt);
        
        return self::streamEncrypt(
            $input,
            $output,
            new EncryptionKey($encKey),
            $firstnonce,
            $mac,
            $config
        );
    }

    /**
     * Decrypt a (file handle)
     *
     * @param $input
     * @param $output
     * @param EncryptionKey $key
     * @return bool
     * @throws CryptoException\InvalidType
     */
    protected static function decryptResource(
        $input,
        $output,
        EncryptionKey $key
    ): bool {
        // Input validation
        if (!\is_resource($input)) {
            throw new CryptoException\InvalidType(
                'Expected input handle to be a resource'
            );
        }
        if (!\is_resource($output)) {
            throw new CryptoException\InvalidType(
                'Expected output handle to be a resource'
            );
        }
        return self::decryptStream(
            new ReadOnlyFile($input),
            new MutableFile($output),
            $key
        );
    }
    
    /**
     * Decrypt a (file handle)
     * 
     * @param $input
     * @param $output
     * @param EncryptionKey $key
     * @return bool
     */
    protected static function decryptStream(
        ReadOnlyFile $input,
        MutableFile $output,
        EncryptionKey $key
    ): bool {
        $input->reset(0);
        // Parse the header, ensuring we get 4 bytes
        $header = $input->readBytes(Halite::VERSION_TAG_LEN);
        
        // Load the config
        $config = self::getConfig($header, 'encrypt');
        
        // Let's grab the first nonce and salt
        $firstnonce = $input->readBytes($config->NONCE_BYTES);
        $hkdfsalt = $input->readBytes($config->HKDF_SALT_LEN);
        
        // Split our keys, begin the HMAC instance
        list ($encKey, $authKey) = self::splitKeys($key, $hkdfsalt, $config);
        $mac = \hash_init('sha256', HASH_HMAC, $authKey);
        
        \hash_update($mac, $header);
        \hash_update($mac, $firstnonce);
        \hash_update($mac, $hkdfsalt);
        
        // This will throw an exception if it fails.
        $old_macs = self::streamVerify($input, \hash_copy($mac), $config);
        
        $ret = self::streamDecrypt(
            $input,
            $output,
            new EncryptionKey($encKey),
            $firstnonce,
            $mac,
            $config,
            $old_macs
        );
        
        unset($encKey);
        unset($authKey);
        unset($firstnonce);
        unset($mac);
        unset($config);
        unset($old_macs);
        return $ret;
    }
    
    /**
     * Seal a (file handle)
     * 
     * @param $input
     * @param $output
     * @param EncryptionPublicKey $publickey
     * @return int
     * @throws CryptoException\InvalidType
     */
    protected static function sealResource(
        $input,
        $output,
        EncryptionPublicKey $publickey
    ): int {
        // Input validation
        if (!\is_resource($input)) {
            throw new CryptoException\InvalidType(
                'Expected input handle to be a resource'
            );
        }
        if (!\is_resource($output)) {
            throw new CryptoException\InvalidType(
                'Expected output handle to be a resource'
            );
        }
        return self::sealStream(
            new ReadOnlyFile($input),
            new MutableFile($output),
            $publickey
        );
    }
    
    /**
     * Seal a (file handle)
     * 
     * @param ReadOnlyFile $input
     * @param MutableFile $output
     * @param EncryptionPublicKey $publickey
     * @return int
     */
    protected static function sealStream(
        ReadOnlyFile $input,
        MutableFile $output,
        EncryptionPublicKey $publickey
    ): int {
        // Generate a new keypair for this encryption
        $eph_kp = KeyFactory::generateEncryptionKeyPair();
            $eph_secret = $eph_kp->getSecretKey();
            $eph_public = $eph_kp->getPublicKey();
        unset($eph_kp);
        
        // Calculate the shared secret key
        $key = AsymmetricCrypto::getSharedSecret($eph_secret, $publickey, true);
        
        // Destroy the secre tkey after we have the shared secret
        unset($eph_secret);
        $config = self::getConfig(Halite::HALITE_VERSION_FILE, 'seal');
        
        // Generate a nonce as per crypto_box_seal
        $nonce = \Sodium\crypto_generichash(
            $eph_public->get().$publickey->get(),
            '',
            \Sodium\CRYPTO_STREAM_NONCEBYTES
        );
        
        // Generate a random HKDF salt
        $hkdfsalt = \Sodium\randombytes_buf($config->HKDF_SALT_LEN);
        
        // Split the keys
        list ($encKey, $authKey) = self::splitKeys($key, $hkdfsalt, $config);
        
        // We no longer need the original key after we split it
        unset($key);
        
        $mac = \hash_init('sha256', HASH_HMAC, $authKey);
        // We no longer need to retain this after we've set up the hash context
        unset($authKey);
        
        $output->writeBytes(Halite::HALITE_VERSION_FILE, Halite::VERSION_TAG_LEN);
        $output->writeBytes($eph_public->get(), \Sodium\CRYPTO_BOX_PUBLICKEYBYTES);
        $output->writeBytes($hkdfsalt, $config->HKDF_SALT_LEN);
        
        \hash_update($mac, Halite::HALITE_VERSION_FILE);
        \hash_update($mac, $eph_public->get());
        \hash_update($mac, $hkdfsalt);
        
        unset($eph_public);
        
        return self::streamEncrypt(
            $input,
            $output,
            new EncryptionKey($encKey),
            $nonce,
            $mac,
            $config
        );
    }
    
    /**
     * Unseal a (file handle)
     * 
     * @param $input
     * @param $output
     * @param EncryptionSecretKey $secretkey
     * @return bool
     * @throws CryptoException\InvalidType
     */
    protected static function unsealResource(
        $input,
        $output,
        EncryptionSecretKey $secretkey
    ): bool {
        // Input validation
        if (!\is_resource($input)) {
            throw new CryptoException\InvalidType(
                'Expected input handle to be a resource'
            );
        }
        if (!\is_resource($output)) {
            throw new CryptoException\InvalidType(
                'Expected output handle to be a resource'
            );
        }
        return self::unsealStream(
            new ReadOnlyFile($input),
            new MutableFile($output),
            $secretkey
        );
    }
    
    /**
     * Unseal a (file handle)
     * 
     * @param ReadOnlyFile $input
     * @param MutableFile $output
     * @param EncryptionSecretKey $secretkey
     *
     * @return bool
     * @throws CryptoException\InvalidKey
     */
    protected static function unsealStream(
        ReadOnlyFile $input,
        MutableFile $output,
        EncryptionSecretKey $secretkey
    ): bool {
        $secret_key = $secretkey->get();
        $public_key = \Sodium\crypto_box_publickey_from_secretkey($secret_key);
        
        // Parse the header, ensuring we get 4 bytes
        $header = $input->readBytes(Halite::VERSION_TAG_LEN);
        // Load the config
        $config = self::getConfig($header, 'seal');
        // Let's grab the public key and salt
        $eph_public = $input->readBytes($config->PUBLICKEY_BYTES);
        $hkdfsalt = $input->readBytes($config->HKDF_SALT_LEN);
        
        $nonce = \Sodium\crypto_generichash(
            $eph_public . $public_key,
            '',
            \Sodium\CRYPTO_STREAM_NONCEBYTES
        );
        
        $ephemeral = new EncryptionPublicKey($eph_public);
        
        $key = AsymmetricCrypto::getSharedSecret(
            $secretkey, 
            $ephemeral,
            true
        );
        list ($encKey, $authKey) = self::splitKeys($key, $hkdfsalt, $config);
        // We no longer need the original key after we split it
        unset($key);
        
        $mac = \hash_init('sha256', HASH_HMAC, $authKey);
        
        \hash_update($mac, $header);
        \hash_update($mac, $eph_public);
        \hash_update($mac, $hkdfsalt);
        
        // This will throw an exception if it fails.
        $old_macs = self::streamVerify($input, \hash_copy($mac), $config);
        
        $ret = self::streamDecrypt(
            $input,
            $output,
            new EncryptionKey($encKey),
            $nonce,
            $mac,
            $config,
            $old_macs
        );
        
        unset($encKey);
        unset($authKey);
        unset($nonce);
        unset($mac);
        unset($config);
        unset($old_macs);
        return $ret;
    }
    
    /**
     * Sign the contents of a file
     * 
     * @param $input (file handle)
     * @param SignatureSecretKey $secretkey
     * @param bool $raw_binary Don't hex encode?
     */
    protected static function signResource(
        $input,
        SignatureSecretKey $secretkey,
        bool $raw_binary = false
    ): string {
        return self::signStream(
            new ReadOnlyFile($input),
            $secretkey,
            $raw_binary
        );
    }
    
    /**
     * Sign the contents of a file
     * 
     * @param ReadOnlyFile $input
     * @param SignatureSecretKey $secretkey
     * @param bool $raw_binary Don't hex encode?
     * @return string
     * @throws CryptoException\InvalidKey
     */
    protected static function signStream(
        ReadOnlyFile $input,
        SignatureSecretKey $secretkey,
        bool $raw_binary = false
    ): string {
        if (!($secretkey instanceof SignatureSecretKey)) {
            throw new CryptoException\InvalidKey(
                'Argument 1: Expected an instance of SignatureSecretKey'
            );
        }
        $csum = self::checksumStream(
            $input,
            $secretkey->derivePublicKey(),
            true
        );
        return AsymmetricCrypto::sign($csum, $secretkey, $raw_binary);
    }
    
    /**
     * Verify the contents of a file
     * 
     * @param $input (file handle)
     * @param SignaturePublicKey $publickey
     * @param string $signature
     * @param bool $raw_binary Don't hex encode?
     * 
     * @return bool
     */
    protected static function verifyResource(
        $input,
        SignaturePublicKey $publickey,
        string $signature,
        bool $raw_binary = false
    ): bool {
        return self::verifyStream(
            new ReadOnlyFile($input),
            $publickey,
            $signature,
            $raw_binary
        );
    }

    /**
     * Verify the contents of a file
     * 
     * @param $input (file handle)
     * @param SignaturePublicKey $publickey
     * @param string $signature
     * @param bool $raw_binary Don't hex encode?
     * 
     * @return bool
     */
    protected static function verifyStream(
        ReadOnlyFile $input,
        SignaturePublicKey $publickey,
        string $signature,
        bool $raw_binary = false
    ): bool {
        $csum = self::checksumStream($input, $publickey, true);
        return AsymmetricCrypto::verify(
            $csum,
            $publickey,
            $signature,
            $raw_binary
        );
    }
    
    /**
     * Get the configuration
     * 
     * @param string $header
     * @param string $mode
     * @return Config
     * @throws CryptoException\InvalidMessage
     */
    protected static function getConfig(
        string $header,
        string $mode = 'encrypt'
    ): Config {
        if (\ord($header[0]) !== 49 || \ord($header[1]) !== 65) {
            throw new CryptoException\InvalidMessage(
                'Invalid version tag'
            );
        }
        $major = \ord($header[2]);
        $minor = \ord($header[3]);
        if ($mode === 'encrypt') {
            return new Config(
                self::getConfigEncrypt($major, $minor)
            );
        } elseif ($mode === 'seal') {
            return new Config(
                self::getConfigSeal($major, $minor)
            );
        } elseif ($mode === 'checksum') {
            return new Config(
                self::getConfigChecksum($major, $minor)
            );
        }
    }
    
    /**
     * Get the configuration for encrypt operations
     * 
     * @param int $major
     * @param int $minor
     * @return array
     * @throws CryptoException\InvalidMessage
     */
    protected static function getConfigEncrypt(int $major, int $minor): array
    {
        if ($major === 1) {
            switch ($minor) {
                case 0:
                    return [
                        'BUFFER' => 1048576,
                        'NONCE_BYTES' => \Sodium\CRYPTO_STREAM_NONCEBYTES,
                        'HKDF_SALT_LEN' => 32,
                        'MAC_SIZE' => 32,
                        'HKDF_SBOX' => 'Halite|EncryptionKey',
                        'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite'
                    ];
            }
        } elseif ($major === 2) {
            switch ($minor) {
                case 0:
                    return [
                        'BUFFER' => 1048576,
                        'NONCE_BYTES' => \Sodium\CRYPTO_STREAM_NONCEBYTES,
                        'HKDF_SALT_LEN' => 32,
                        'MAC_SIZE' => 32,
                        'HKDF_SBOX' => 'Halite|EncryptionKey',
                        'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite'
                    ];
            }
        }
        throw new CryptoException\InvalidMessage(
            'Invalid version tag'
        );
    }
    
    /**
     * Get the configuration for seal operations
     * 
     * @param int $major
     * @param int $minor
     * @return array
     * @throws CryptoException\InvalidMessage
     */
    protected static function getConfigSeal(int $major, int $minor): array
    {
        if ($major === 1) {
            switch ($minor) {
                case 0:
                    return [
                        'BUFFER' => 1048576,
                        'HKDF_SALT_LEN' => 32,
                        'MAC_SIZE' => 32,
                        'PUBLICKEY_BYTES' => \Sodium\CRYPTO_BOX_PUBLICKEYBYTES,
                        'HKDF_SBOX' => 'Halite|EncryptionKey',
                        'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite'
                    ];
            }
        } elseif ($major === 2) {
            switch ($minor) {
                case 0:
                    return [
                        'BUFFER' => 1048576,
                        'HKDF_SALT_LEN' => 32,
                        'MAC_SIZE' => 32,
                        'PUBLICKEY_BYTES' => \Sodium\CRYPTO_BOX_PUBLICKEYBYTES,
                        'HKDF_SBOX' => 'Halite|EncryptionKey',
                        'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite'
                    ];
            }
        }
        throw new CryptoException\InvalidMessage(
            'Invalid version tag'
        );
    }
    
    /**
     * Get the configuration for encrypt operations
     * 
     * @param int $major
     * @param int $minor
     * @return array
     * @throws CryptoException\InvalidMessage
     */
    protected static function getConfigChecksum(int $major, int $minor): array
    {
        if ($major === 1) {
            switch ($minor) {
                case 0:
                    return [
                        'CHECKSUM_PUBKEY' => false,
                        'BUFFER' => 1048576,
                        'HASH_LEN' => \Sodium\CRYPTO_GENERICHASH_BYTES_MAX
                    ];
            }
        } elseif ($major === 2) {
            switch ($minor) {
                case 0:
                    return [
                        'CHECKSUM_PUBKEY' => true,
                        'BUFFER' => 1048576,
                        'HASH_LEN' => \Sodium\CRYPTO_GENERICHASH_BYTES_MAX
                    ];
            }
        }
        throw new CryptoException\InvalidMessage(
            'Invalid version tag'
        );
    }
    
    /**
     * Split a key using HKDF
     * 
     * @param KeyInterface $master
     * @param string $salt
     * @param Config $config
     * @return string[]
     */
    protected static function splitKeys(
        KeyInterface $master,
        string $salt = '',
        Config $config = null
    ): array {
        $binary = $master->get();
        return [
            Util::hkdfBlake2b(
                $binary,
                \Sodium\CRYPTO_SECRETBOX_KEYBYTES,
                $config->HKDF_SBOX,
                $salt
            ),
            Util::hkdfBlake2b(
                $binary,
                \Sodium\CRYPTO_AUTH_KEYBYTES,
                $config->HKDF_AUTH,
                $salt
            )
        ];
    }
    
    /**
     * Stream encryption - Do not call directly
     * 
     * @param ReadOnlyFile $input
     * @param MutableFile $output
     * @param EncryptionKey $encKey
     * @param string $nonce
     * @param resource $mac (hash context)
     * @param Config $config
     * @return int
     * @throws CryptoException\AccessDenied
     * @throws CryptoException\FileModified
     * @throws CryptoException\InvalidKey
     */
    final private static function streamEncrypt(
        ReadOnlyFile $input,
        MutableFile $output,
        EncryptionKey $encKey,
        string $nonce,
        $mac,
        Config $config
    ): int {
        $initHash = $input->getHash();
        // Begin the streaming decryption
        $size = $input->getSize();
        while ($input->remainingBytes() > 0) {
            $read = $input->readBytes(
                ($input->getPos() + $config->BUFFER) > $size
                    ? ($size - $input->getPos())
                    : $config->BUFFER
            );
            
            $encrypted = \Sodium\crypto_stream_xor(
                $read,
                $nonce,
                $encKey->get()
            );
            \hash_update($mac, $encrypted);
            $output->writeBytes($encrypted);
            \Sodium\increment($nonce);
        }
        \Sodium\memzero($nonce);
        // Check that our input file was not modified before we MAC it
        if (!\hash_equals($input->gethash(), $initHash)) {
            throw new CryptoException\FileModified(
                'Read-only file has been modified since it was opened for reading'
            );
        }
        return $output->writeBytes(
            \hash_final($mac, true)
        );
    }
    
    /**
     * Stream decryption - Do not call directly
     * 
     * @param ReadOnlyFile $input
     * @param MutableFile $output
     * @param Key $encKey
     * @param string $nonce
     * @param resource $mac (hash context)
     * @param Config $config
     * @return bool
     * @throws CryptoException\AccessDenied
     * @throws CryptoException\CannotPerformOperation
     * @throws CryptoException\FileModified
     * @throws CryptoException\InvalidKey
     * @throws CryptoException\InvalidMessage
     */
    final private static function streamDecrypt(
        ReadOnlyFile $input,
        MutableFile $output,
        EncryptionKey $encKey,
        string $nonce,
        $mac,
        Config $config,
        array &$chunk_macs
    ): bool {
        $start = $input->getPos();
        $cipher_end = $input->getSize() - $config->MAC_SIZE;
        // Begin the streaming decryption
        $input->reset($start);
        
        while ($input->remainingBytes() > $config->MAC_SIZE) {
            if (($input->getPos() + $config->BUFFER) > $cipher_end) {
                $read = $input->readBytes(
                    $cipher_end - $input->getPos()
                );
            } else {
                $read = $input->readBytes($config->BUFFER);
            }
            \hash_update($mac, $read);
            $calcMAC = \hash_copy($mac);
            if ($calcMAC === false) {
                throw new CryptoException\CannotPerformOperation(
                    'An unknown error has occurred'
                );
            }
            $calc = \hash_final($calcMAC, true);
            
            if (empty($chunk_macs)) {
                throw new CryptoException\InvalidMessage(
                    'Invalid message authentication code'
                );
            } else {
                $chkmac = \array_shift($chunk_macs);
                if (!\hash_equals($chkmac, $calc)) {
                    throw new CryptoException\InvalidMessage(
                        'Invalid message authentication code'
                    );
                }
            }
            
            $decrypted = \Sodium\crypto_stream_xor(
                $read,
                $nonce,
                $encKey->get()
            );
            $output->writeBytes($decrypted);
            \Sodium\increment($nonce);
        }
        \Sodium\memzero($nonce);
        return true;
    }
    
    /**
     * Recalculate and verify the HMAC of the input file
     * 
     * @param resource $input
     * @param resource $mac (hash context)
     * @param Config $config
     * 
     * @return array Hashes of various chunks
     * @throws CryptoException\CannotPerformOperation
     * @throws CryptoException\InvalidMessage
     */
    final private static function streamVerify(
        ReadOnlyFile $input,
        $mac,
        Config $config
    ): array {
        $start = $input->getPos();
        
        $cipher_end = $input->getSize() - $config->MAC_SIZE;
        $input->reset($cipher_end);
        $stored_mac = $input->readBytes($config->MAC_SIZE);
        $input->reset($start);
        
        $chunk_macs = [];
        
        $break = false;
        while (!$break && $input->getPos() < $cipher_end) {
            /**
             * Would a full BUFFER read put it past the end of the
             * ciphertext? If so, only return a portion of the file.
             */
            if ($input->getPos() + $config->BUFFER >= $cipher_end) {
                $break = true;
                $read = $input->readBytes($cipher_end - $input->getPos());
            } else {
                $read = $input->readBytes($config->BUFFER);
            }
            
            /**
             * We're updating our HMAC and nothing else
             */
            \hash_update($mac, $read);
            
            /**
             * Store a MAC of each chunk
             */
            $chunkMAC = \hash_copy($mac);
            if ($chunkMAC === false) {
                throw new CryptoException\CannotPerformOperation(
                    'An unknown error has occurred'
                );
            }
            $chunk_macs []= \hash_final($chunkMAC, true);
        }
        
        /**
         * We should now have enough data to generate an identical HMAC
         */
        $finalHMAC = \hash_final($mac, true);
        
        /**
         * Use hash_equals() to be timing-invariant
         */
        if (!\hash_equals($finalHMAC, $stored_mac)) {
            throw new CryptoException\InvalidMessage(
                'Invalid message authentication code'
            );
        }
        $input->reset($start);
        return $chunk_macs;
    }
}

For more information send a message to info at phpclasses dot org.