function test_hash($algo, $init) { echo "\n{$algo}, incremental: "; $h = hash_init($algo); for ($i = 0; $i < 10; ++$i) { hash_update($h, '' . $init * 2 + $i * 17); } echo '(copying state) '; $h2 = hash_copy($h); for ($i = 0; $i < 10; ++$i) { hash_update($h, '' . $init * 2 + $i * 19); } var_dump(hash_final($h)); echo "\n{$algo}, from copied state: "; var_dump(hash_final($h2)); echo "\n{$algo}, HMAC, incremental: "; $h = hash_init($algo, HASH_HMAC, 'HMAC key. It can be very long, but in this case it will be rehashed to fit the block size of the hashing algorithm...' . $init * 147); for ($i = 0; $i < 10; ++$i) { hash_update($h, '' . $init * 4 + $i * 7); } //echo '(copying state) '; //$h2 = hash_copy($h);// causes PHP crashes sometimes, reported as PHP Bug #52240 for ($i = 0; $i < 10; ++$i) { hash_update($h, '' . $init * 3 + $i * 11); } var_dump(hash_final($h)); //echo "\n$algo, HMAC, from copied state: "; //var_dump(hash_final($h2));// BUG IN PHP, HMAC key is not copied, but only referenced ... hash_final on $h clears the HMAC key in $h2 too... reported as PHP Bug #52240 echo "\n{$algo}, at once, short data: "; var_dump(hash($algo, 'some string to be hashed ... ' . $init * 123 . ' ...')); echo "\n{$algo}, at once, HMAC: "; var_dump(hash_hmac($algo, 'some string to be hashed ... ' . $init * 123 . ' ...', 'HMAC key. It can be very long, but in this case it will be rehashed to fit the block size of the hashing algorithm.')); }
/** * Copy a hashing context. * (PHP 5 >= 5.3.0) * * @param resource * @return resource */ public final function copy($context) { if (!is_resource($context)) { return false; } return hash_copy($context); }
/** * {@inheritDoc} */ public function digest() { // Copy the context so we can keep using the hasher $context_copy = hash_copy($this->context); // Calculate the digest $digest = hash_final($this->context, true); // Set our context to the copied one, since the old one is now finalized $this->context = $context_copy; return $digest; }
function Save() { // ASSERTION - There is no volatile data if ($this->status == 0) { // We haven't called Open() yet so just return the status, everything can be left defaults on Load() return array('status' => $this->status); } // Convert hash_ctx into CRC field if ($this->hash_ctx !== false && $this->hash_len != 0) { $copy_ctx = hash_copy($this->hash_ctx); list($hash_ctx) = array_values(unpack('N', hash_final($copy_ctx, true))); } else { $hash_ctx = false; } return array('status' => $this->status, 'cipher_spec' => $this->cipher_spec, 'key' => $this->key, 'iv' => $this->iv, 'real_blocksize' => $this->real_blocksize, 'blocksize' => $this->blocksize, 'data' => $this->data, 'data_len' => $this->data_len, 'crc' => $this->crc, 'hash_ctx' => $hash_ctx, 'hash_len' => $this->hash_len, 'totalsize' => $this->totalsize, 'encsize' => $this->encsize, 'last_cipher' => $this->last_cipher, 'buffer_disk' => $this->buffer_disk === false ? false : $this->buffer_disk->Save(), 'volatile' => $this->volatile, 'volatile_len' => $this->volatile_len, 'header_pos' => $this->header_pos); }
function Save() { // Return the state $state = array('rolling_size' => $this->rolling_size); // Convert hash_ctx into CRC field if ($this->rolling_size !== false) { $state['rolling_crc'] = $this->rolling_crc; if ($this->rolling_hash_ctx !== false && $this->rolling_hash_len) { $copy_ctx = hash_copy($this->rolling_hash_ctx); $state['rolling_hash_ctx'] = hexdec(hash_final($copy_ctx, false)); $state['rolling_hash_len'] = $this->rolling_hash_len; } else { $state['rolling_hash_ctx'] = false; } } return $state; }
/** * Recalculate and verify the HMAC of the input file * * @param resource $input * @param resource|string $mac (hash context) * @param Config $config * * @return array Hashes of various chunks * @throws CryptoException\CannotPerformOperation * @throws CryptoException\InvalidMessage */ private static final 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 */ if ($config->USE_BLAKE2B) { \Sodium\crypto_generichash_update($mac, $read); $chunkMAC = '' . $mac; $chunk_macs[] = \Sodium\crypto_generichash_final($chunkMAC, $config->MAC_SIZE); } 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 */ if ($config->USE_BLAKE2B) { $finalHMAC = \Sodium\crypto_generichash_final($mac, $config->MAC_SIZE); } else { $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; }
<?php $algos = hash_algos(); foreach ($algos as $algo) { var_dump($algo); $orig = hash_init($algo); hash_update($orig, "I can't remember anything"); $copy = hash_copy($orig); var_dump(hash_final($orig)); var_dump(hash_final($copy)); } foreach ($algos as $algo) { var_dump($algo); $orig = hash_init($algo); hash_update($orig, "I can't remember anything"); $copy = hash_copy($orig); var_dump(hash_final($orig)); hash_update($copy, "Can’t tell if this is true or dream"); var_dump(hash_final($copy)); } echo "Done\n";
/** * @param bool|false $raw * @return string */ public function result($raw = false) { $context = hash_copy($this->context); return hash_final($context, $raw); }
<?php $c = hash_init("crc32"); hash_update($c, "Hello"); $d = hash_copy($c); hash_update($c, "World"); hash_update($d, "Goodbye"); var_dump(hash_final($c)); var_dump(hash("crc32", "HelloWorld")); var_dump(hash_final($d)); var_dump(hash("crc32", "HelloGoodbye"));
<?php $h = hash_init('crc32b', HASH_HMAC, '123456'); $h2 = hash_copy($h); var_dump(hash_final($h)); $h3 = hash_copy($h2); var_dump(hash_final($h2)); var_dump(hash_final($h3));
/** * Decrypt the contents of a file handle $inputHandle and store the results * in $outputHandle using HKDF of $key to decrypt then verify * * @param resource $inputHandle * @param resource $outputHandle * @param Key $key * @return boolean */ public static function decryptResource($inputHandle, $outputHandle, Key $key) { // Because we don't have strict typing in PHP 5 if (!\is_resource($inputHandle)) { throw new Ex\InvalidInput('Input handle must be a resource!'); } if (!\is_resource($outputHandle)) { throw new Ex\InvalidInput('Output handle must be a resource!'); } // Parse the header. $header = self::readBytes($inputHandle, Core::HEADER_VERSION_SIZE); $config = self::getFileVersionConfigFromHeader($header, Core::CURRENT_FILE_VERSION); // Let's add this check before anything if (!\in_array($config->hashFunctionName(), \hash_algos())) { throw new Ex\CannotPerformOperationException('The specified hash function does not exist'); } // Let's grab the file salt. $file_salt = self::readBytes($inputHandle, $config->saltByteSize()); // For storing MACs of each buffer chunk $macs = []; /** * 1. We need to decode some values from our files */ /** * Let's split our keys * * $ekey -- Encryption Key -- used for AES */ $ekey = Core::HKDF($config->hashFunctionName(), $key->getRawBytes(), $config->keyByteSize(), $config->encryptionInfoString(), $file_salt, $config); /** * $akey -- Authentication Key -- used for HMAC */ $akey = Core::HKDF($config->hashFunctionName(), $key->getRawBytes(), $config->keyByteSize(), $config->authenticationInfoString(), $file_salt, $config); /** * Grab our IV from the encrypted message * * It should be the first N blocks of the file (N = 16) */ $ivsize = \openssl_cipher_iv_length($config->cipherMethod()); $iv = self::readBytes($inputHandle, $ivsize); // How much do we increase the counter after each buffered encryption to prevent nonce reuse $inc = $config->bufferByteSize() / $config->blockByteSize(); $thisIv = $iv; /** * Let's grab our MAC * * It should be the last N blocks of the file (N = 32) */ if (\fseek($inputHandle, -1 * $config->macByteSize(), SEEK_END) === false) { throw new Ex\CannotPerformOperationException('Cannot seek to beginning of MAC within input file'); } // Grab our last position of ciphertext before we read the MAC $cipher_end = \ftell($inputHandle); if ($cipher_end === false) { throw new Ex\CannotPerformOperationException('Cannot read input file'); } --$cipher_end; // We need to subtract one // We keep our MAC stored in this variable $stored_mac = self::readBytes($inputHandle, $config->macByteSize()); /** * We begin recalculating the HMAC for the entire file... */ $hmac = \hash_init($config->hashFunctionName(), HASH_HMAC, $akey); if ($hmac === false) { throw new Ex\CannotPerformOperationException('Cannot initialize a hash context'); } /** * Reset file pointer to the beginning of the file after the header */ if (\fseek($inputHandle, Core::HEADER_VERSION_SIZE, SEEK_SET) === false) { throw new Ex\CannotPerformOperationException('Cannot read seek within input file'); } /** * Set it to the first non-salt and non-IV byte */ if (\fseek($inputHandle, $config->saltByteSize() + $ivsize, SEEK_CUR) === false) { throw new Ex\CannotPerformOperationException('Cannot read seek input file to beginning of ciphertext'); } /** * 2. Let's recalculate the MAC */ /** * Let's initialize our $hmac hasher with our Salt and IV */ \hash_update($hmac, $header); \hash_update($hmac, $file_salt); \hash_update($hmac, $iv); $hmac2 = \hash_copy($hmac); $break = false; while (!$break) { /** * First, grab the current position */ $pos = \ftell($inputHandle); if ($pos === false) { throw new Ex\CannotPerformOperationException('Could not get current position in input file during decryption'); } /** * Would a full DBUFFER read put it past the end of the * ciphertext? If so, only return a portion of the file. */ if ($pos + $config->bufferByteSize() >= $cipher_end) { $break = true; $read = self::readBytes($inputHandle, $cipher_end - $pos + 1); } else { $read = self::readBytes($inputHandle, $config->bufferByteSize()); } if ($read === false) { throw new Ex\CannotPerformOperationException('Could not read input file during decryption'); } /** * We're updating our HMAC and nothing else */ \hash_update($hmac, $read); /** * Store a MAC of each chunk */ $chunkMAC = \hash_copy($hmac); if ($chunkMAC === false) { throw new Ex\CannotPerformOperationException('Cannot duplicate a hash context'); } $macs[] = \hash_final($chunkMAC); } /** * We should now have enough data to generate an identical HMAC */ $finalHMAC = \hash_final($hmac, true); /** * 3. Did we match? */ if (!Core::hashEquals($finalHMAC, $stored_mac)) { throw new Ex\InvalidCiphertextException('Message Authentication failure; tampering detected.'); } /** * 4. Okay, let's begin decrypting */ /** * Return file pointer to the first non-header, non-IV byte in the file */ if (\fseek($inputHandle, $config->saltByteSize() + $ivsize + Core::HEADER_VERSION_SIZE, SEEK_SET) === false) { throw new Ex\CannotPerformOperationException('Could not move the input file pointer during decryption'); } /** * Should we break the writing? */ $breakW = false; /** * This loop writes plaintext to the destination file: */ while (!$breakW) { /** * Get the current position */ $pos = \ftell($inputHandle); if ($pos === false) { throw new Ex\CannotPerformOperationException('Could not get current position in input file during decryption'); } /** * Would a full BUFFER read put it past the end of the * ciphertext? If so, only return a portion of the file. */ if ($pos + $config->bufferByteSize() >= $cipher_end) { $breakW = true; $read = self::readBytes($inputHandle, $cipher_end - $pos + 1); } else { $read = self::readBytes($inputHandle, $config->bufferByteSize()); } /** * Recalculate the MAC, compare with the one stored in the $macs * array to ensure attackers couldn't tamper with the file * after MAC verification */ \hash_update($hmac2, $read); $calcMAC = \hash_copy($hmac2); if ($calcMAC === false) { throw new Ex\CannotPerformOperationException('Cannot duplicate a hash context'); } $calc = \hash_final($calcMAC); if (empty($macs)) { throw new Ex\InvalidCiphertextException('File was modified after MAC verification'); } elseif (!Core::hashEquals(\array_shift($macs), $calc)) { throw new Ex\InvalidCiphertextException('File was modified after MAC verification'); } $thisIv = Core::incrementCounter($thisIv, $inc, $config); /** * Perform the AES decryption. Decrypts the message. */ $decrypted = \openssl_decrypt($read, $config->cipherMethod(), $ekey, OPENSSL_RAW_DATA, $thisIv); /** * Test for decryption faulure */ if ($decrypted === false) { throw new Ex\CannotPerformOperationException('OpenSSL decryption error'); } /** * Write the plaintext out to the output file */ self::writeBytes($outputHandle, $decrypted, Core::ourStrlen($decrypted)); } return true; }
/** * @ignore */ public function __clone() { $this->m_hashingContext = hash_copy($this->m_hashingContext); }
public function __clone() { // copy the hash so we dont tamper with the original (e.g. hash_final // will pad and corrupt it if we get the value of it.) $this->hash = hash_copy($this->hash); }
/** * Decrypts a file-backed resource with either a key or a password. * * @param resource $inputHandle * @param resource $outputHandle * @param KeyOrPassword $secret * * @throws Defuse\Crypto\Exception\EnvironmentIsBrokenException * @throws Defuse\Crypto\Exception\IOException * @throws Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException */ public static function decryptResourceInternal($inputHandle, $outputHandle, KeyOrPassword $secret) { if (!\is_resource($inputHandle)) { throw new Ex\IOException('Input handle must be a resource!'); } if (!\is_resource($outputHandle)) { throw new Ex\IOException('Output handle must be a resource!'); } /* Make sure the file is big enough for all the reads we need to do. */ $stat = \fstat($inputHandle); if ($stat['size'] < Core::MINIMUM_CIPHERTEXT_SIZE) { throw new Ex\WrongKeyOrModifiedCiphertextException('Input file is too small to have been created by this library.'); } /* Check the version header. */ $header = self::readBytes($inputHandle, Core::HEADER_VERSION_SIZE); if ($header !== Core::CURRENT_VERSION) { throw new Ex\WrongKeyOrModifiedCiphertextException('Bad version header.'); } /* Get the salt. */ $file_salt = self::readBytes($inputHandle, Core::SALT_BYTE_SIZE); /* Get the IV. */ $ivsize = Core::BLOCK_BYTE_SIZE; $iv = self::readBytes($inputHandle, $ivsize); /* Derive the authentication and encryption keys. */ $keys = $secret->deriveKeys($file_salt); $ekey = $keys->getEncryptionKey(); $akey = $keys->getAuthenticationKey(); /* We'll store the MAC of each buffer-sized chunk as we verify the * actual MAC, so that we can check them again when decrypting. */ $macs = []; /* $thisIv will be incremented after each call to the decryption. */ $thisIv = $iv; /* How many blocks do we encrypt at a time? We increment by this value. */ $inc = Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE; /* Get the HMAC. */ if (\fseek($inputHandle, -1 * Core::MAC_BYTE_SIZE, SEEK_END) === false) { throw new Ex\IOException('Cannot seek to beginning of MAC within input file'); } /* Get the position of the last byte in the actual ciphertext. */ $cipher_end = \ftell($inputHandle); if ($cipher_end === false) { throw new Ex\IOException('Cannot read input file'); } /* We have the position of the first byte of the HMAC. Go back by one. */ --$cipher_end; /* Read the HMAC. */ $stored_mac = self::readBytes($inputHandle, Core::MAC_BYTE_SIZE); /* Initialize a streaming HMAC state. */ $hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey); if ($hmac === false) { throw new Ex\EnvironmentIsBrokenException('Cannot initialize a hash context'); } /* Reset file pointer to the beginning of the file after the header */ if (\fseek($inputHandle, Core::HEADER_VERSION_SIZE, SEEK_SET) === false) { throw new Ex\IOException('Cannot read seek within input file'); } /* Seek to the start of the actual ciphertext. */ if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize, SEEK_CUR) === false) { throw new Ex\IOException('Cannot seek input file to beginning of ciphertext'); } /* PASS #1: Calculating the HMAC. */ \hash_update($hmac, $header); \hash_update($hmac, $file_salt); \hash_update($hmac, $iv); $hmac2 = \hash_copy($hmac); $break = false; while (!$break) { $pos = \ftell($inputHandle); if ($pos === false) { throw new Ex\IOException('Could not get current position in input file during decryption'); } /* Read the next buffer-sized chunk (or less). */ if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) { $break = true; $read = self::readBytes($inputHandle, $cipher_end - $pos + 1); } else { $read = self::readBytes($inputHandle, Core::BUFFER_BYTE_SIZE); } /* Update the HMAC. */ \hash_update($hmac, $read); /* Remember this buffer-sized chunk's HMAC. */ $chunk_mac = \hash_copy($hmac); if ($chunk_mac === false) { throw new Ex\EnvironmentIsBrokenException('Cannot duplicate a hash context'); } $macs[] = \hash_final($chunk_mac); } /* Get the final HMAC, which should match the stored one. */ $final_mac = \hash_final($hmac, true); /* Verify the HMAC. */ if (!Core::hashEquals($final_mac, $stored_mac)) { throw new Ex\WrongKeyOrModifiedCiphertextException('Integrity check failed.'); } /* PASS #2: Decrypt and write output. */ /* Rewind to the start of the actual ciphertext. */ if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize + Core::HEADER_VERSION_SIZE, SEEK_SET) === false) { throw new Ex\IOException('Could not move the input file pointer during decryption'); } $at_file_end = false; while (!$at_file_end) { $pos = \ftell($inputHandle); if ($pos === false) { throw new Ex\IOException('Could not get current position in input file during decryption'); } /* Read the next buffer-sized chunk (or less). */ if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) { $at_file_end = true; $read = self::readBytes($inputHandle, $cipher_end - $pos + 1); } else { $read = self::readBytes($inputHandle, Core::BUFFER_BYTE_SIZE); } /* Recalculate the MAC (so far) and compare it with the one we * remembered from pass #1 to ensure attackers didn't change the * ciphertext after MAC verification. */ \hash_update($hmac2, $read); $calc_mac = \hash_copy($hmac2); if ($calc_mac === false) { throw new Ex\EnvironmentIsBrokenException('Cannot duplicate a hash context'); } $calc = \hash_final($calc_mac); if (empty($macs)) { throw new Ex\WrongKeyOrModifiedCiphertextException('File was modified after MAC verification'); } elseif (!Core::hashEquals(\array_shift($macs), $calc)) { throw new Ex\WrongKeyOrModifiedCiphertextException('File was modified after MAC verification'); } /* Decrypt this buffer-sized chunk. */ $decrypted = \openssl_decrypt($read, Core::CIPHER_METHOD, $ekey, OPENSSL_RAW_DATA, $thisIv); if ($decrypted === false) { throw new Ex\EnvironmentIsBrokenException('OpenSSL decryption error'); } /* Write the plaintext to the output file. */ self::writeBytes($outputHandle, $decrypted, Core::ourStrlen($decrypted)); /* Increment the IV by the amount of blocks in a buffer. */ $thisIv = Core::incrementCounter($thisIv, $inc); /* WARNING: Usually, unless the file is a multiple of the buffer * size, $thisIv will contain an incorrect value here on the last * iteration of this loop. */ } }
function Save() { if ($this->hash_ctx !== false && $this->job !== false && $this->job['hash_len'] != 0) { $copy_ctx = hash_copy($this->hash_ctx); list($crc) = array_values(unpack('N', hash_final($copy_ctx, true))); // Save it in the job $this->job['saved_hash_ctx'] = $crc; } }
function Save() { // Return the state $state = array('rolling_buffer' => $this->rolling_buffer); // Convert hash_ctx into CRC field if ($this->rolling_buffer !== false) { $state['rolling_len'] = $this->rolling_len; $state['rolling_size'] = $this->rolling_size; $state['rolling_crc'] = $this->rolling_crc; if ($this->rolling_hash_ctx !== false && $this->rolling_hash_len != 0) { $copy_ctx = hash_copy($this->rolling_hash_ctx); list($state['rolling_hash_ctx']) = array_values(unpack('N', hash_final($copy_ctx, true))); $state['rolling_hash_len'] = $this->rolling_hash_len; } else { $state['rolling_hash_ctx'] = false; } $state['rolling_tempfile'] = $this->rolling_tempfile !== false ? $this->rolling_tempfile->Save() : false; $state['rolling_gzipname'] = $this->rolling_gzipname; } return $state; }
public function __clone() { $handle = hash_copy($this->getHandle()); $this->setHandle($handle); }
/** * Recalculate and verify the HMAC of the input file * * @param resource $input * @param Key $authKey * @param resource $mac (hash context) * @param &array $config * @return Hashes of various chunks * @throws FileAlert\AccessDenied */ private static final function streamVerify($input, $mac, array $config) { $start = \ftell($input); if (\fseek($input, -1 * $config['MAC_SIZE'], SEEK_END) === false) { throw new CryptoAlert\CannotPerformOperation('Stream error'); } $cipher_end = \ftell($input) - 1; $stored_mac = self::readBytes($input, $config['MAC_SIZE']); if (\fseek($input, $start, SEEK_SET) === false) { throw new CryptoAlert\CannotPerformOperation('Stream error'); } $chunk_macs = []; $break = false; while (!$break) { /** * First, grab the current position */ $pos = \ftell($input); if ($pos === false) { throw new CryptoAlert\CannotPerformOperation('Stream error'); } if ($pos >= $cipher_end) { break; } /** * Would a full BUFFER read put it past the end of the * ciphertext? If so, only return a portion of the file. */ if ($pos + $config['BUFFER'] >= $cipher_end) { $break = true; $read = self::readBytes($input, $cipher_end - $pos + 1); } else { $read = self::readBytes($input, $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 CryptoAlert\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); if (!\hash_equals($finalHMAC, $stored_mac)) { throw new CryptoAlert\InvalidMessage('Invalid message authentication code'); } if (\fseek($input, $start, SEEK_SET) === false) { throw new CryptoAlert\CannotPerformOperation('Stream error'); } return $chunk_macs; }
<?php $r = hash_init("md5"); var_dump(hash_copy()); var_dump(hash_copy($r)); var_dump(hash_copy($r, $r)); echo "Done\n";
/** * {@inheritdoc} */ public function __clone() { if ($this->context) { $this->context = hash_copy($this->context); } }
<?php $ctx = hash_init('md5'); hash_update($ctx, 'The quick brown fox '); hash_update($ctx, 'jumped over the lazy dog.'); echo hash_final($ctx); $ctx2 = hash_copy($ctx); var_dump($ctx2); var_dump(hash_update($ctx, 'The quick brown fox ')); var_dump(hash_final($ctx));