diff --git a/docs/src/Introduction.rst b/docs/src/Introduction.rst index e8d673e..c063d21 100644 --- a/docs/src/Introduction.rst +++ b/docs/src/Introduction.rst @@ -17,7 +17,7 @@ Also, very few of those extensions support on-the-fly encryption/decryption. Cryptal was created to work around these issues by providing a unified interface & transparent support for on-the-fly encryption/decryption -using stream wrappers. +using stream filters. .. vim: ts=4 et diff --git a/docs/src/Usage.rst b/docs/src/Usage.rst index f3dc7cf..1e33fef 100644 --- a/docs/src/Usage.rst +++ b/docs/src/Usage.rst @@ -1,7 +1,7 @@ Usage ##### -Cryptal provides support for the following features: +Cryptal provides support for the following main features: * Encryption/decryption * Hashes (also known as message digests) @@ -9,7 +9,7 @@ Cryptal provides support for the following features: For each feature, two sets of interfaces are provided: -* PHP streams, which hide the complexity of the operations +* PHP stream filters, which hide the complexity of the operations and provide transparent support for the features. This mode of operation is usually adequate for network protocols @@ -31,12 +31,12 @@ The rest of this document describes the interfaces available for each feature. Encryption/decryption ===================== -Using streams -------------- +Using stream filters +-------------------- .. warning:: - When using the stream mode, the library relies mostly on PHP code + When using the stream filters, the library relies mostly on PHP code to handle encryption/decryption. The underlying library is only used to provide the cryptographic primitives for the selected cipher in `ECB `_ mode. @@ -46,26 +46,22 @@ Using streams cannot be safely erased from memory and may linger there even after you are done processing the data. - If you are concerned about these issues, do not use these streams. - -.. note:: - - A stream context is required when using this interface, - to pass all necessary settings to the library. - - See the section on `Encryption/decryption contexts`_ for more information. + If you are concerned about these issues, do not use the stream filters. Encryption ~~~~~~~~~~ -Encrypting some data is easy: +Encrypting data is easy: .. sourcecode:: inline-php // Initialize the library \fpoirotte\Cryptal::init(); + // Open a new stream + $stream = stream_socket_client('tcp://localhost:12345'); + // Create an encryption context (see below) $ctx = stream_context_create( array( @@ -81,39 +77,48 @@ Encrypting some data is easy: ) ); - $plaintext = "Some secret message we want to transmit securely"; + // Add an encryption layer to the stream. + $filter = stream_filter_append( + $stream, + 'cryptal.encrypt', + // We want the data to be encrypted as we write it. + STREAM_FILTER_WRITE, + array( + // Encrypt the data using AES-128 in CTR mode. + 'algorithm' => CipherEnum::CIPHER_AES_128(), + 'mode' => ModeEnum::MODE_CTR(), - // Open a new encryption stream, using the AES-128 cipher in CTR mode. - // See fpoirotte\Cryptal\CipherEnum and fpoirotte\Cryptal\ModeEnum - // for a list of valid ciphers/modes. - $encrypt = fopen("cryptal.encrypt://MODE_CTR/CIPHER_AES_128", 'w+', false, $ctx); - - // Feed the stream with data to encrypt. - fwrite($encrypt, $plaintext); - - // The encrypted data can be retrieved using fread(). - // Make sure the $length argument is at least twice - // the cipher's block size. - // - // fread() will return an empty string if there is not enough - // data in the buffer, a block of encrypted data, or false - // on error (eg. when the given $length is too small). - while ($data = fread($encrypt, 1024)) { - // Do something with the data... - } + // Secret key. + // Size must be compatible with the cipher's expectations. + 'key' => '0123456789abcdef', - // Notify the stream that the end of the data has been reached. - fflush($encrypt); + // Initialization Vector. + // Size must be compatible with the cipher's expectations. + 'iv' => 'abcdef0123456789', + ) + ); - // After fflush() has been called, you should keep reading - // from the stream until no more data can be retrieved. - while ($data = fread($encrypt, 1024)) { - // Do something with the data... + // We make sure the filter was successfully applied. + if (false === $filter) { + throw new \Exception('Could not add the encryption layer'); } - // After that, the stream will be unusable and a new one - // must be created if further data must be processed. + // Now that the encryption layer is in place, we can write + // to the stream just like we would normally do. + // Any data written to the stream will be encrypted on the fly. + fwrite($stream, "Some secret message we want to transmit securely"); + +.. warning:: + When adding the filter, the 3rd argument to ``stream_filter_append()`` + (``$read_write``) should be set to either ``STREAM_FILTER_WRITE`` + if the encryption should happen during writes (eg. via ``fwrite()``), + or ``STREAM_FILTER_READ`` if it should happen during reads (eg. via + ``fread()`` or ``fgets()``). + + Using the default value (``STREAM_FILTER_ALL``) means the same filter + is applied to both operations, which is not supported and may produce + unexpected results. Here's another example, this time using Authenticated Encryption with Associated Data (AEAD): @@ -127,29 +132,49 @@ Decryption ~~~~~~~~~~ Decryption works the same way. Just substitute ``cryptal.decrypt`` in place -of ``cryptal.encrypt`` when creating the stream. +of ``cryptal.encrypt`` when adding the filter. When using Authenticated Encryption, @TODO -Encryption/decryption contexts -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A stream context is needed to configure the encryption/decryption process. +Filter parameters for ``cryptal.encrypt``/``cryptal.decrypt`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The following table lists available options: +When using streams, the following options may be used when adding the filter +to control the way encryption/decryption is performed: -.. list-table:: Available options in encryption/decryption contexts - :widths: 10 35 55 +.. list-table:: Parameters for cryptal.encrypt/cryptal.decrypt + :widths: 10 5 35 50 :header-rows: 1 * - Name + - Optional - Expected type - Description + * - ``mode`` + - yes + - ``\fpoirotte\Cryptal\ModeEnum`` + - The cipher's mode of operations to use. + + This parameter is important as the various modes offer different + security garantees. Make sure you have read documentation on the + various modes and their implications before setting this value. + + * - ``algorithm`` + - yes + - ``\fpoirotte\Cryptal\CipherEnum`` + - The cipher algorithm to use to encrypt/decrypt the data. + + This parameter is important as the various ciphers offer different + security garantees. Make sure you have read documentation on the + various ciphers and their limitations before setting this value. + * - ``allowUnsafe`` + - no - boolean - Whether userland PHP implementations may be used or not. + Defaults to ``false``. While those implementations add support for some rarely used algorithms, they are usually way slower than implementations @@ -157,7 +182,7 @@ The following table lists available options: Also, those implementations are considered unsafe because they cannot protect the application from certain classes of attacks like - PHP extensions usually do (eg. timing attacks). + PHP extensions usually do (eg. side-channel attacks). Last but not least, when using those implementations, secret values may reside in memory for longer than is actually necessary @@ -165,80 +190,79 @@ The following table lists available options: making them vulnerable to memory forensic techniques and such. * - ``data`` + - no - string - Additional Data to authenticate when using `Authenticated Encryption `_ - * - ``IV`` + * - ``iv`` + - yes/no - string - - Initialization Vector for the cipher + - Initialization Vector for the cipher. + Whether this parameter is optional or not depends of the + encryption/decryption mode used. * - ``key`` + - yes - string - Symmetric key to use for encryption/decryption * - ``padding`` - - Instance of ``\fpoirotte\Cryptal\PaddingInterface`` - - Padding scheme to use (defaults to no padding) + - no + - ``\fpoirotte\Cryptal\PaddingInterface`` + - Padding scheme to use. Defaults to no padding. * - ``tag`` + - no - string - Authentication tag for the current block. This value is set by the - stream wrapper during encryption of a block. It should be set manually - when decrypting, before passing a block to decrypt to the stream - wrapper. + filter during encryption of a block. It should be set manually + when decrypting, before passing a block to decrypt to the stream. * - ``tagLength`` + - no - integer - Desired tag length (in bytes) when using `Authenticated Encryption `_. - Defaults to 16 bytes (128 bits). Only used during encryption, - as it can be deduced from the ``tag``'s actual length when decrypting. - - -To set an option, use ``stream_context_set_option()``: - -.. sourcecode:: inline-php - - stream_context_set_option($stream_or_context, 'cryptal', $option, $value); + Defaults to 16 bytes (128 bits). + + This parameters is only used during encryption, as it can be deduced + from the ``tag``'s actual length when decrypting. -To retrieve the current value for an option, -use ``stream_context_get_options()``: - -.. sourcecode:: inline-php - - $options = stream_context_get_options($stream_or_context); - $padding = $options['cryptal']['padding']; - echo "Padding scheme in use: " . get_class($padding) . PHP_EOL; Padding ~~~~~~~ -By default, no padding is applied (ie. the padding scheme is set to -an instance of ``fpoirotte\Cryptal\Padding\None``) when using streams. +By default, no padding is applied to streams (ie. the padding scheme +is set to an instance of ``fpoirotte\Cryptal\Padding\None``). If you need to use another padding scheme, you can easily swap the default -for an alternate implementation. Just set the ``padding`` context option -to an instance of the padding scheme to use before opening the stream: +for an alternate implementation. Just set the ``padding`` filter parameter +to an instance of the padding scheme to use when adding the filter: .. sourcecode:: inline-php use fpoirotte\Cryptal\Padding\AnsiX923; - $ctx = stream_context_create( + // Open the stream + $stream = fopen(..., 'wb'); + + stream_filter_append( + $stream, + 'cryptal.encrypt', + STREAM_FILTER_WRITE, array( - 'cryptal' => array( 'key' => '0123456789abcdef', 'IV' => 'abcdef0123456789', + 'algorithm' => CipherEnum::CIPHER_AES_128(), + 'mode' => ModeEnum::MODE_CTR(), - // Use the ANSI X.923 padding scheme instead of PKCS#7. + // Use the ANSI X.923 padding scheme. 'padding' => new AnsiX923, - ) ) ); - $encrypt = fopen("cryptal.encrypt://MODE_CTR/CIPHER_AES_128", 'w+', false, $ctx); // Do something with the stream... @@ -300,8 +324,11 @@ Associated Data (AEAD): Hashes (message digests) ======================== -Using streams -------------- +Using stream filters +-------------------- + +Replicating ``md5_file()`` using Cryptal +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Hashing data using streams is really easy. For example, to obtain an MD5 message digest for a file (similar to what the PHP ``md5_file()`` function @@ -312,16 +339,80 @@ returns), the following snippet can be used: // Initialize the library \fpoirotte\Cryptal::init(); - // Open the hashing stream & a regular file stream. - $hashStream = fopen("cryptal.hash://HASH_MD5", 'w+b'); - $fileStream = fopen("/path/to/some.data", "rb"); + // Open the binary file for reading. + $fp = fopen("/path/to/some.data", "rb"); - // Pass data from the file to the hashing stream. - stream_copy_to_stream($fileStream, $hashStream); + // Add the hashing filter to the stream. + stream_filter_append( + $fp, + 'cryptal.hash', + // We want to compute the hash based on data read from the file. + STREAM_FILTER_READ, + array( + 'algorithm' => HashEnum::HASH_MD5() + ) + ); // Read the resulting message digest (returned in raw form). // The MD5 algorithm produces a 128-bit hash (16 bytes). - $hash = fread($hashStream, 16); + $hash = stream_get_contents($fp); + +.. waning:: + + When adding the filter, the 3rd argument to ``stream_filter_append()`` + (``$read_write``) should be set to either ``STREAM_FILTER_WRITE`` + if the hashing should happen during writes (eg. via ``fwrite()``), + or ``STREAM_FILTER_READ`` if it should happen during reads (eg. via + ``fread()`` or ``fgets()``). + + Using the default value (``STREAM_FILTER_ALL``) means the same filter + is applied to both operations, which is not supported and may produce + unexpected results. + + +Filter parameters for ``cryptal.hash`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using streams, the following options may be used when adding the filter +to control the way the message digest is computed: + +.. list-table:: Parameters for cryptal.hash + :widths: 10 5 35 50 + :header-rows: 1 + + * - Name + - Optional + - Expected type + - Description + + * - ``algorithm`` + - yes + - ``\fpoirotte\Cryptal\HashEnum`` + - The algorithm to use to hash the data. + + This parameter is important as the various algorithms offer different + security garantees. Make sure you have read documentation on the + various algorithms and their limitations before setting this value. + + * - ``allowUnsafe`` + - no + - boolean + - Whether userland PHP implementations may be used or not. + Defaults to ``false``. + + While those implementations add support for some rarely used + algorithms, they are usually way slower than implementations + based on PHP extensions. + + Also, those implementations are considered unsafe because they cannot + protect the application from certain classes of attacks like + PHP extensions usually do (eg. side-channel attacks). + + Last but not least, when using those implementations, secret values + may reside in memory for longer than is actually necessary + (possibly even longer than the program's actual execution time), + making them vulnerable to memory forensic techniques and such. + Using the registry ------------------ @@ -386,8 +477,11 @@ less predictable. Before computing any MAC, we suggest that you get some documentation first on whatever algorithm you are planning to use to know its requirements. -Using streams -------------- +Using stream filters +-------------------- + +Quick example: HMAC-MD5 on a file +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To compute a MAC using the stream interface, just use code similar to this one: @@ -396,27 +490,111 @@ To compute a MAC using the stream interface, just use code similar to this one: // Initialize the library \fpoirotte\Cryptal::init(); - // Create a MAC context, holding the secret key - $ctx = stream_context_create( + // Open the binary file for reading. + $macGiver = fopen("/path/to/some.data", "rb"); + + // Add the hashing filter to the stream. + stream_filter_append( + $macGiver, + 'cryptal.mac', + // We want to compute the MAC based on data read from the file. + STREAM_FILTER_READ, array( - 'cryptal' => array( - // Secret key. - // Size must be compatible with the algorithms used. - 'key' => '0123456789abcdef', - ) + 'algorithm' => MacEnum::MAC_HMAC(), + 'innerAlgorithm' => HashEnum::HASH_MD5(), + + // Size must be compatible with the algorithms in use. + 'key' => '0123456789abcdef', ) ); - // Open the MAC stream & a regular file stream. - $macGiver = fopen("cryptal.mac://MAC_HMAC/HASH_MD5", 'w+b', false, $ctx); - $fileStream = fopen("/path/to/some.data", "rb"); - - // Pass data from the file to the MAC stream. - stream_copy_to_stream($fileStream, $macGiver); - // Retrieve the Message Authentication Code in raw binary form. // The HMAC-MD5 algorithm produces a 128-bit hash (16 bytes). - $hash = fread($macGiver, 16); + $mac = stream_get_contents($macGiver); + + +.. warning:: + + When adding the filter, the 3rd argument to ``stream_filter_append()`` + (``$read_write``) should be set to either ``STREAM_FILTER_WRITE`` + if the tag computation should happen during writes (eg. via ``fwrite()``), + or ``STREAM_FILTER_READ`` if it should happen during reads (eg. via + ``fread()`` or ``fgets()``). + + Using the default value (``STREAM_FILTER_ALL``) means the same filter + is applied to both operations, which is not supported and may produce + unexpected results. + + +Filter parameters for ``cryptal.mac`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using streams, the following options may be used when adding the filter +to control the way the message authentication code is computed: + +.. list-table:: Parameters for cryptal.mac + :widths: 10 5 35 50 + :header-rows: 1 + + * - Name + - Optional + - Expected type + - Description + + * - ``algorithm`` + - yes + - ``\fpoirotte\Cryptal\MacEnum`` + - Outer algorithm to use to perform the computation. + + This parameter is important as the various algorithms offer different + security garantees. Make sure you have read documentation on the + various algorithms and their limitations before setting this value. + + * - ``innerAlgorithm`` + - yes + - ``\fpoirotte\Cryptal\SubAlgorithmAbstractEnum`` + - Inner algorithm to use to perform the computation. + + Depending on the selected ``algorithm``, this parameter should be set + to either an instance of ``\fpoirotte\Cryptal\CipherEnum`` or + ``\fpoirotte\Cryptal\HashEnum``. + + This parameter is important as the various algorithms offer different + security garantees. Make sure you have read documentation on the + various algorithms and their limitations before setting this value. + + * - ``allowUnsafe`` + - no + - boolean + - Whether userland PHP implementations may be used or not. + Defaults to ``false``. + + While those implementations add support for some rarely used + algorithms, they are usually way slower than implementations + based on PHP extensions. + + Also, those implementations are considered unsafe because they cannot + protect the application from certain classes of attacks like + PHP extensions usually do (eg. side-channel attacks). + + Last but not least, when using those implementations, secret values + may reside in memory for longer than is actually necessary + (possibly even longer than the program's actual execution time), + making them vulnerable to memory forensic techniques and such. + + * - ``nonce`` + - yes/no + - string + - Nonce to make the output less predictable. + Whether this parameter is optional or not depends on the + selected ``algorithm``/``innerAlgorithm``. + + * - ``key`` + - yes + - string + - Symmetric key to use for the computation + + Using the registry ------------------ diff --git a/phpunit.xml b/phpunit.xml index 27c4489..2e9672d 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,6 +10,9 @@ tests/API/Ciphers + + tests/API/Filters + tests/API/MessageAuthenticators diff --git a/src/Cryptal.php b/src/Cryptal.php index 28f341f..f3d9f8b 100644 --- a/src/Cryptal.php +++ b/src/Cryptal.php @@ -28,16 +28,18 @@ public static function init() return false; } - $streams = array( - 'cryptal.encrypt' => "\\fpoirotte\Cryptal\\Streams\\Crypto", - 'cryptal.decrypt' => "\\fpoirotte\Cryptal\\Streams\\Crypto", - 'cryptal.hash' => "\\fpoirotte\Cryptal\\Streams\\Hash", - 'cryptal.mac' => "\\fpoirotte\Cryptal\\Streams\\Mac", + $filters = array( + 'cryptal.binify' => "\\fpoirotte\Cryptal\\Filters\\Binify", + 'cryptal.hexify' => "\\fpoirotte\Cryptal\\Filters\\Hexify", + 'cryptal.encrypt' => "\\fpoirotte\Cryptal\\Filters\\Crypto", + 'cryptal.decrypt' => "\\fpoirotte\Cryptal\\Filters\\Crypto", + 'cryptal.hash' => "\\fpoirotte\Cryptal\\Filters\\Hash", + 'cryptal.mac' => "\\fpoirotte\Cryptal\\Filters\\Mac", ); - foreach ($streams as $stream => $cls) { - if (!stream_wrapper_register($stream, $cls)) { - throw new \Exception("Failed to register '$stream' stream wrapper"); + foreach ($filters as $filter => $cls) { + if (!stream_filter_register($filter, $cls)) { + throw new \Exception("Failed to register '$filter' stream filter"); } } diff --git a/src/Cryptal/Filters/Binify.php b/src/Cryptal/Filters/Binify.php new file mode 100644 index 0000000..6eed390 --- /dev/null +++ b/src/Cryptal/Filters/Binify.php @@ -0,0 +1,52 @@ +buffer = ''; + } + + public function filter($in, $out, &$consumed, $closing) + { + $res = PSFS_FEED_ME; + while (true) { + $bucket = stream_bucket_make_writeable($in); + if ($bucket) { + $this->buffer .= $bucket->data; + } + + + $available = strlen($this->buffer); + if ($available >= 2) { + $consume = $available - ($available % 2); + $data = substr($this->buffer, 0, $consume); + + if (strspn($data, '1234567890abcdefABCDEF') !== $consume) { + // The input contains non-hexadecimal data. + throw new \RuntimeException('Invalid data in input'); + } + + $outBucket = stream_bucket_new($this->stream, pack('H*', $data)); + stream_bucket_append($out, $outBucket); + + $this->buffer = (string) substr($this->buffer, $consume); + $consumed += $consume; + $res = PSFS_PASS_ON; + } + + if (!$bucket) { + if ($closing && $this->buffer !== '') { + // The input contains an odd number of bytes and is thus invalid. + throw new \RuntimeException('Odd number of bytes in input'); + } + + return $res; + } + } + } +} diff --git a/src/Cryptal/Filters/Crypto.php b/src/Cryptal/Filters/Crypto.php new file mode 100644 index 0000000..e94139b --- /dev/null +++ b/src/Cryptal/Filters/Crypto.php @@ -0,0 +1,143 @@ +params['algorithm']) || + !is_object($this->params['algorithm']) || + !($this->params['algorithm'] instanceof CipherEnum)) { + throw new \InvalidArgumentException('Invalid algorithm'); + } + + if (!isset($this->params['mode']) || + !is_object($this->params['mode']) || + !($this->params['mode'] instanceof ModeEnum)) { + throw new \InvalidArgumentException('Invalid mode'); + } + + if (!isset($this->params['key']) || !is_string($this->params['key'])) { + throw new \InvalidArgumentException('Missing or invalid key'); + } + + $padding = new \fpoirotte\Cryptal\Padding\None(); + if (isset($this->params['padding'])) { + $padding = $this->params['padding']; + } + if (!is_object($padding) || !($padding instanceof PaddingInterface)) { + throw new \InvalidArgumentException('Invalid padding scheme'); + } + + $tagLength = CryptoInterface::DEFAULT_TAG_LENGTH; + if (isset($this->params['tagLength'])) { + $tagLength = $this->params['tagLength']; + } + if (!is_integer($tagLength) || $tagLength < 0) { + throw new \InvalidArgumentException('Invalid tag length'); + } + + $iv = isset($this->params['iv']) ? $this->params['iv'] : ''; + if (!is_string($iv)) { + throw new \InvalidArgumentException('Invalid initialization vector'); + } + + // Make sure the selected mode is supported. + if (!isset($this->params['mode'])) { + throw new \InvalidArgumentException('No mode specified'); + } + $mode = "\\fpoirotte\\Cryptal\\Modes\\" . substr($this->params['mode'], strlen('MODE_')); + $interfaces = class_implements($mode, true); + if (!$interfaces || !in_array("fpoirotte\Cryptal\SymmetricModeInterface", $interfaces)) { + throw new \InvalidArgumentException('Unsupported mode'); + } + + $allowUnsafe = isset($this->params['allowUnsafe']) ? (bool) $this->params['allowUnsafe'] : false; + $cipher = Registry::buildCipher( + $this->params['algorithm'], + ModeEnum::MODE_ECB(), + new \fpoirotte\Cryptal\Padding\None(), + $this->params['key'], + $tagLength, + $allowUnsafe + ); + + $this->buffer = ''; + $this->blockSize = $cipher->getBlockSize(); + $this->padding = $padding; + $this->mode = new $mode( + $cipher, + $iv, + $tagLength + ); + if ('cryptal.decrypt' === $this->filtername && $this->mode instanceof AsymmetricModeInterface) { + $this->method = 'decrypt'; + } else { + $this->method = 'encrypt'; + } + return true; + } + + public function filter($in, $out, &$consumed, $closing) + { + $res = PSFS_FEED_ME; + $method = $this->method; +# $options = stream_context_get_options($this->stream); + + while (true) { + $bucket = stream_bucket_make_writeable($in); + if ($bucket) { + $this->buffer .= $bucket->data; + } elseif ('cryptal.encrypt' === $this->filtername && $closing) { + // Add the padding scheme + $missing = $this->blockSize - (strlen($this->buffer) % $this->blockSize); + $this->buffer .= $this->padding->getPaddingData($this->blockSize, $missing); + } + + $available = strlen($this->buffer); + $nbBlocks = ($available - ($available % $this->blockSize)) / $this->blockSize; + + if ($nbBlocks > 0) { + $consume = $nbBlocks * $this->blockSize; + $outBuffer = ''; + for ($i = 0; $i < $nbBlocks; $i++) { + $outBuffer .= $this->mode->$method( + substr($this->buffer, $this->blockSize * $i, $this->blockSize), + $this->stream + ); + } + + if (!$bucket && 'cryptal.decrypt' === $this->filtername && $closing) { + // Remove the padding scheme + $padLen = $this->padding->getPaddingSize($outBuffer, $this->blockSize); + if ($padLen) { + $outBuffer = (string) substr($outBuffer, 0, -$padLen); + } + } + + stream_bucket_append($out, stream_bucket_new($this->stream, $outBuffer)); + $this->buffer = (string) substr($this->buffer, $consume); + $consumed += $consume; + $res = PSFS_PASS_ON; + } + + if (!$bucket) { + return $res; + } + } + } +} diff --git a/src/Cryptal/Filters/Hash.php b/src/Cryptal/Filters/Hash.php new file mode 100644 index 0000000..84888e2 --- /dev/null +++ b/src/Cryptal/Filters/Hash.php @@ -0,0 +1,39 @@ +params['algorithm']) || + !is_object($this->params['algorithm']) || + !($this->params['algorithm'] instanceof HashEnum)) { + throw new \InvalidArgumentException('Invalid algorithm'); + } + + $allowUnsafe = isset($this->params['allowUnsafe']) ? (bool) $this->params['allowUnsafe'] : false; + $this->context = Registry::buildHash($this->params['algorithm'], $allowUnsafe); + return true; + } + + public function filter($in, $out, &$consumed, $closing) + { + while ($bucket = stream_bucket_make_writeable($in)) { + $this->context->update($bucket->data); + $consumed += $bucket->datalen; + } + + if ($closing) { + $bucket = stream_bucket_new($this->stream, $this->context->finalize(true)); + stream_bucket_append($out, $bucket); + } + + return PSFS_PASS_ON; + } +} diff --git a/src/Cryptal/Filters/Hexify.php b/src/Cryptal/Filters/Hexify.php new file mode 100644 index 0000000..7f5dda1 --- /dev/null +++ b/src/Cryptal/Filters/Hexify.php @@ -0,0 +1,16 @@ +datalen; + $outBucket = stream_bucket_new($this->stream, bin2hex($bucket->data)); + stream_bucket_append($out, $outBucket); + } + return PSFS_PASS_ON; + } +} diff --git a/src/Cryptal/Filters/Mac.php b/src/Cryptal/Filters/Mac.php new file mode 100644 index 0000000..fb362c5 --- /dev/null +++ b/src/Cryptal/Filters/Mac.php @@ -0,0 +1,63 @@ +params['algorithm']) || + !is_object($this->params['algorithm']) || + !($this->params['algorithm'] instanceof MacEnum)) { + throw new \InvalidArgumentException('Invalid algorithm'); + } + + if (!isset($this->params['innerAlgorithm']) || + !is_object($this->params['innerAlgorithm']) || + !($this->params['innerAlgorithm'] instanceof SubAlgorithmAbstractEnum)) { + throw new \InvalidArgumentException('Invalid inner algorithm'); + } + + if (!isset($this->params['key']) || !is_string($this->params['key'])) { + throw new \InvalidArgumentException('Missing or invalid key'); + } + + $nonce = isset($this->params['nonce']) ? $this->params['nonce'] : ''; + if (!is_string($nonce)) { + throw new \InvalidArgumentException('Invalid nonce'); + } + + $allowUnsafe = isset($this->params['allowUnsafe']) ? (bool) $this->params['allowUnsafe'] : false; + $this->context = Registry::buildMac( + $this->params['algorithm'], + $this->params['innerAlgorithm'], + $this->params['key'], + $nonce, + $allowUnsafe + ); + return true; + } + + public function filter($in, $out, &$consumed, $closing) + { + while ($bucket = stream_bucket_make_writeable($in)) { + $this->context->update($bucket->data); + $consumed += $bucket->datalen; + } + + if ($closing) { + $bucket = stream_bucket_new($this->stream, $this->context->finalize(true)); + stream_bucket_append($out, $bucket); + } + + return PSFS_PASS_ON; + } +} diff --git a/src/Cryptal/Modes/CCM.php b/src/Cryptal/Modes/CCM.php index c00ed1c..da08612 100644 --- a/src/Cryptal/Modes/CCM.php +++ b/src/Cryptal/Modes/CCM.php @@ -91,15 +91,11 @@ protected function checkum($M, $A) /// Increment the value of the counter by one. protected function incrementCounter($c) { + $carry = 1; for ($i = $this->L - 1; $i >= 0; $i--) { // chr() takes care of overflows automatically. - $c[$i] = chr(ord($c[$i]) + 1); - - // Stop, unless the incremented generated an overflow. - // In that case, we continue to propagate the carry. - if ("\x00" !== $c[$i]) { - break; - } + $c[$i] = chr(ord($c[$i]) + $carry); + $carry &= ("\x00" === $c[$i]); } return $c; } diff --git a/src/Cryptal/Modes/CTR.php b/src/Cryptal/Modes/CTR.php index f95147e..5c2b8fc 100644 --- a/src/Cryptal/Modes/CTR.php +++ b/src/Cryptal/Modes/CTR.php @@ -35,15 +35,11 @@ public function __construct(CryptoInterface $cipher, $iv, $tagLength) /// Increment the value of the counter by one. protected function incrementCounter() { + $carry = 1; for ($i = $this->blockSize - 1; $i >= 0; $i--) { // chr() takes care of overflows automatically. - $this->counter[$i] = chr(ord($this->counter[$i]) + 1); - - // Stop, unless the incremented generated an overflow. - // In that case, we continue to propagate the carry. - if ("\x00" !== $this->counter[$i]) { - break; - } + $this->counter[$i] = chr(ord($this->counter[$i]) + $carry); + $carry &= ("\x00" === $this->counter[$i]); } } diff --git a/src/Cryptal/Registry.php b/src/Cryptal/Registry.php index 825c68a..1397bf6 100644 --- a/src/Cryptal/Registry.php +++ b/src/Cryptal/Registry.php @@ -78,6 +78,7 @@ public function addCipher($packageName, $cls, CipherEnum $cipher, ModeEnum $mode throw new \InvalidArgumentException("$cls does not implement $iface"); } $this->metadata['crypt']["$cipher:$mode"][] = array($packageName, $cls, $type); + return $this; } public function addHash($packageName, $cls, HashEnum $algo, ImplementationTypeEnum $type) @@ -88,6 +89,7 @@ public function addHash($packageName, $cls, HashEnum $algo, ImplementationTypeEn throw new \InvalidArgumentException("$cls does not implement $iface"); } $this->metadata['hash']["$algo"][] = array($packageName, $cls, $type); + return $this; } public function addMac($packageName, $cls, MacEnum $algo, ImplementationTypeEnum $type) @@ -98,6 +100,7 @@ public function addMac($packageName, $cls, MacEnum $algo, ImplementationTypeEnum throw new \InvalidArgumentException("$cls does not implement $iface"); } $this->metadata['mac']["$algo"][] = array($packageName, $cls, $type); + return $this; } public function removeAlgorithms($packageName) @@ -111,6 +114,7 @@ public function removeAlgorithms($packageName) } } } + return $this; } public function load($registerDefaultAlgorithms = true) @@ -124,84 +128,103 @@ public function load($registerDefaultAlgorithms = true) } if ($registerDefaultAlgorithms) { - // Ciphers - $this->addCipher( - '', - 'fpoirotte\\Cryptal\\DefaultAlgorithms\\ChaCha20Openssh', - CipherEnum::CIPHER_CHACHA20_OPENSSH(), - ModeEnum::MODE_ECB(), - ImplementationTypeEnum::TYPE_USERLAND() - ); + $this->registerDefaultAlgorithms(); + } + return $this; + } + + public function registerDefaultAlgorithms() + { + // Ciphers + $this->addCipher( + '', + 'fpoirotte\\Cryptal\\DefaultAlgorithms\\ChaCha20Openssh', + CipherEnum::CIPHER_CHACHA20_OPENSSH(), + ModeEnum::MODE_ECB(), + ImplementationTypeEnum::TYPE_USERLAND() + ); + $this->addCipher( + '', + 'fpoirotte\\Cryptal\\DefaultAlgorithms\\ChaCha20', + CipherEnum::CIPHER_CHACHA20(), + ModeEnum::MODE_ECB(), + ImplementationTypeEnum::TYPE_USERLAND() + ); + $camellia = array( + CipherEnum::CIPHER_CAMELIA_128(), + CipherEnum::CIPHER_CAMELIA_192(), + CipherEnum::CIPHER_CAMELIA_256(), + ); + foreach ($camellia as $cipher) { $this->addCipher( '', - 'fpoirotte\\Cryptal\\DefaultAlgorithms\\ChaCha20', - CipherEnum::CIPHER_CHACHA20(), + 'fpoirotte\\Cryptal\\DefaultAlgorithms\\Camellia', + $cipher, ModeEnum::MODE_ECB(), ImplementationTypeEnum::TYPE_USERLAND() ); - $camellia = array( - CipherEnum::CIPHER_CAMELIA_128(), - CipherEnum::CIPHER_CAMELIA_192(), - CipherEnum::CIPHER_CAMELIA_256(), - ); - foreach ($camellia as $cipher) { - $this->addCipher( - '', - 'fpoirotte\\Cryptal\\DefaultAlgorithms\\Camellia', - $cipher, - ModeEnum::MODE_ECB(), - ImplementationTypeEnum::TYPE_USERLAND() - ); - } - - // Hashes - $algos = array( - HashEnum::HASH_CRC32(), - HashEnum::HASH_MD5(), - HashEnum::HASH_SHA1(), - ); - foreach ($algos as $algo) { - $this->addHash( - '', - 'fpoirotte\\Cryptal\\DefaultAlgorithms\\Hash', - $algo, - ImplementationTypeEnum::TYPE_COMPILED() - ); - } + } - // MACs - $this->addMac( + // Hashes + $algos = array( + HashEnum::HASH_CRC32(), + HashEnum::HASH_MD5(), + HashEnum::HASH_SHA1(), + ); + foreach ($algos as $algo) { + $this->addHash( '', - 'fpoirotte\\Cryptal\\DefaultAlgorithms\\Cmac', - MacEnum::MAC_CMAC(), - ImplementationTypeEnum::TYPE_USERLAND() + 'fpoirotte\\Cryptal\\DefaultAlgorithms\\Hash', + $algo, + ImplementationTypeEnum::TYPE_COMPILED() ); + } + + // MACs + $this->addMac( + '', + 'fpoirotte\\Cryptal\\DefaultAlgorithms\\Cmac', + MacEnum::MAC_CMAC(), + ImplementationTypeEnum::TYPE_USERLAND() + ); + $this->addMac( + '', + 'fpoirotte\\Cryptal\\DefaultAlgorithms\\Poly1305', + MacEnum::MAC_POLY1305(), + ImplementationTypeEnum::TYPE_USERLAND() + ); + $algos = array( + MacEnum::MAC_UMAC_32(), + MacEnum::MAC_UMAC_64(), + MacEnum::MAC_UMAC_96(), + MacEnum::MAC_UMAC_128(), + ); + foreach ($algos as $algo) { $this->addMac( '', - 'fpoirotte\\Cryptal\\DefaultAlgorithms\\Poly1305', - MacEnum::MAC_POLY1305(), + 'fpoirotte\\Cryptal\\DefaultAlgorithms\\Umac', + $algo, ImplementationTypeEnum::TYPE_USERLAND() ); - $algos = array( - MacEnum::MAC_UMAC_32(), - MacEnum::MAC_UMAC_64(), - MacEnum::MAC_UMAC_96(), - MacEnum::MAC_UMAC_128(), - ); - foreach ($algos as $algo) { - $this->addMac( - '', - 'fpoirotte\\Cryptal\\DefaultAlgorithms\\Umac', - $algo, - ImplementationTypeEnum::TYPE_USERLAND() - ); - } } + + return $this; } public function save() { file_put_contents(self::$path, serialize($this->metadata)); + return $this; + } + + public function reset() + { + $this->metadata = array( + 'crypt' => array(), + 'hash' => array(), + 'mac' => array(), + ); + return $this; } protected static function findCipher(CipherEnum $cipher, ModeEnum $mode, $allowUnsafe) @@ -348,15 +371,6 @@ public static function buildMac( return new $cls($algo, $subAlgo, $key, $nonce); } - public function reset() - { - $this->metadata = array( - 'crypt' => array(), - 'hash' => array(), - 'mac' => array(), - ); - } - public function getSupportedCiphers() { $res = array(); diff --git a/src/Cryptal/Streams/Crypto.php b/src/Cryptal/Streams/Crypto.php deleted file mode 100644 index 9b3dc2e..0000000 --- a/src/Cryptal/Streams/Crypto.php +++ /dev/null @@ -1,322 +0,0 @@ -done && !strlen($this->buffer); - } - - // @codingStandardsIgnoreStart - /** - * Open a new stream. - * - * \param string $path - * URL that was passed to the original function. - * - * \param string $mode - * Mode used to open the stream. - * - * \param int $options - * Additional flags set by the streams API. - * - * \param string $opened_path - * A variable that will be filled with the full path - * for the stream on success. - * - * \retval bool - * \b true on success, \b false on failure. - */ - public function stream_open($path, $mode, $options, &$opened_path) - { - // @codingStandardsIgnoreEnd - if (null === $this->context) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Missing cryptographic context', E_USER_ERROR); - } - return false; - } - $ctxOptions = stream_context_get_options($this->context); - - if (false === strpos($mode, '+')) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Invalid mode', E_USER_ERROR); - } - return false; - } - - $iv = ''; - if (isset($ctxOptions['cryptal']['IV'])) { - $iv = (string) $ctxOptions['cryptal']['IV']; - } - - $tagLength = 0; - if (isset($ctxOptions['cryptal']['tagLength'])) { - $tagLength = (int) $ctxOptions['cryptal']['tagLength']; - } - - $padding = new \fpoirotte\Cryptal\Padding\Zero(); - if (isset($ctxOptions['cryptal']['padding'])) { - $padding = $ctxOptions['cryptal']['padding']; - } - - if (!isset($ctxOptions['cryptal']['key'])) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Invalid cryptographic context', E_USER_ERROR); - } - return false; - } - - if ($tagLength < 0) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Invalid tag length', E_USER_ERROR); - } - return false; - } - - if (!($padding instanceof \fpoirotte\Cryptal\PaddingInterface)) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Invalid padding scheme', E_USER_ERROR); - } - return false; - } - - $parts = parse_url($path); - if ($parts === false || !isset($parts['host'], $parts['path'])) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Invalid path', E_USER_ERROR); - } - return false; - } - - if (!strncasecmp($parts['host'], 'MODE_', 5)) { - $mode = $parts['host']; - } else { - $mode = 'MODE_' . strtoupper($parts['host']); - } - - if (!strncasecmp($parts['path'], '/CIPHER_', 8)) { - $cipher = substr($parts['path'], 1); - } else { - $cipher = 'CIPHER_' . strtoupper(substr($parts['path'], 1)); - } - - - $this->padding = $padding; - $this->buffer = ''; - $this->done = false; - $this->direction = $parts['scheme']; - $allowUnsafe = isset($ctxOptions['cryptal']['allowUnsafe']) ? - (bool) $ctxOptions['cryptal']['allowUnsafe'] : false; - - try { - $cipherObj = Registry::buildCipher( - CipherEnum::$cipher(), - ModeEnum::MODE_ECB(), - new \fpoirotte\Cryptal\Padding\None, - $ctxOptions['cryptal']['key'], - 0, - $allowUnsafe - ); - $this->blockSize = $cipherObj->getBlockSize(); - } catch (\Exception $e) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Could not create data processor: ' . $e, E_USER_WARNING); - } - return false; - } - - try { - // Make sure the selected mode is supported. - $mode = "\\fpoirotte\\Cryptal\\Modes\\" . substr($mode, strlen('MODE_')); - $interfaces = class_implements($mode, true); - if (!$interfaces || !in_array("fpoirotte\Cryptal\SymmetricModeInterface", $interfaces)) { - throw new \Exception('Unsupported mode'); - } - - $this->mode = new $mode( - $cipherObj, - $iv, - $tagLength - ); - } catch (\Exception $e) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Could not create operation mode: ' . $e, E_USER_WARNING); - } - return false; - } - - // Some modes of operation use the exact same process for both - // encryption & decryption (eg. OFB, CTR, ...). - // We just redirect the call for those modes to avoid code duplication. - if ('cryptal.decrypt' === $parts['scheme'] && $this->mode instanceof AsymmetricModeInterface) { - $this->method = 'decrypt'; - } else { - $this->method = 'encrypt'; - } - - // Update the context with the actual padding scheme in use. - stream_context_set_option($this->context, 'cryptal', 'padding', $padding); - - $opened_path = $path; - return true; - } - - // @codingStandardsIgnoreStart - /** - * Read data from the stream. - * - * \param int $count - * Requested read count. This value must be large enough - * to hold twice the cipher's block size in data. - * - * \retval bool - * \b false is returned on error (eg. when the requested - * read count is too small). - * - * \retval string - * Data read from the stream. This will be an empty string - * if there is not enough data in the stream's buffer yet, - * or the encrypted/decrypted data otherwise. - */ - public function stream_read($count) - { - // @codingStandardsIgnoreEnd - - if ($count < 2 * $this->blockSize) { - return false; - } - - $nbBlocks = (int) strlen($this->buffer) / $this->blockSize; - - // We do not have enough data in the buffer yet. - if (!$this->done && $nbBlocks < 2) { - return ""; - } - - if (!$nbBlocks) { - return ""; - } - - // Number of blocks we would like to keep in the buffer. - $target = 2; - if ($this->done) { - if ('cryptal.decrypt' === $this->direction && $nbBlocks <= 2 || - 'cryptal.encrypt' === $this->direction) { - $target = 0; - } - } - - // Encrypt/decrypt as much blocks as possible, - // while still retaining enough data in the buffer. - $method = $this->method; - $res = ''; - for ($i = 0; $i < 2 && $nbBlocks > $target; $i++, $nbBlocks--) { - $block = (string) substr($this->buffer, 0, $this->blockSize); - $this->buffer = (string) substr($this->buffer, $this->blockSize); - $res .= $this->mode->$method($block, $this->context); - } - - if ('cryptal.decrypt' === $this->direction && 0 === $target && 0 === $nbBlocks) { - // We were decrypting the last blocks. - // Remove the padding and return the final result. - $padLen = $this->padding->getPaddingSize($res, $this->blockSize); - return $padLen ? (string) substr($res, 0, -$padLen) : $res; - } - - return $res; - } - - // @codingStandardsIgnoreStart - /** - * Notify the wrapper that no more data will be sent to it. - * - * \retval bool - * \b true if the notification is acknowledged, - * \b false otherwise. - */ - public function stream_flush() - { - // @codingStandardsIgnoreEnd - if ($this->done) { - return false; - } - - // Add a padding only when encrypting. - if ('cryptal.encrypt' === $this->direction) { - $missing = $this->blockSize - (strlen($this->buffer) % $this->blockSize); - $this->buffer .= $this->padding->getPaddingData($this->blockSize, $missing); - } - - $this->done = true; - return true; - } - - // @codingStandardsIgnoreStart - /** - * Push data into the stream wrapper. - * - * \param string $data - * Data to add to the stream's buffer. - * - * \retval int - * Size of the data that's effectively been added - * to the buffer. This may be less (even zero) - * than the length of the given data. - */ - public function stream_write($data) - { - // @codingStandardsIgnoreEnd - if ($this->done) { - return 0; - } - - $this->buffer .= $data; - return strlen($data); - } -} diff --git a/src/Cryptal/Streams/Hash.php b/src/Cryptal/Streams/Hash.php deleted file mode 100644 index 447bc8c..0000000 --- a/src/Cryptal/Streams/Hash.php +++ /dev/null @@ -1,143 +0,0 @@ -context) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Missing cryptographic context', E_USER_ERROR); - } - return false; - } - $ctxOptions = stream_context_get_options($this->context); - - if (false === strpos($mode, '+')) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Invalid mode', E_USER_ERROR); - } - return false; - } - - $parts = parse_url($path); - if ($parts === false || !isset($parts['host'])) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Invalid path', E_USER_ERROR); - } - return false; - } - - if (!strncasecmp($parts['host'], 'HASH_', 5)) { - $algo = $parts['host']; - } else { - $algo = 'HASH_' . strtoupper($parts['host']); - } - - $allowUnsafe = isset($ctxOptions['cryptal']['allowUnsafe']) ? - (bool) $ctxOptions['cryptal']['allowUnsafe'] : false; - - try { - $this->implementation = Registry::buildHash(HashEnum::$algo(), $allowUnsafe); - } catch (\Exception $e) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Could not create data processor: ' . $e, E_USER_WARNING); - } - return false; - } - - $opened_path = $path; - return true; - } - - // @codingStandardsIgnoreStart - /** - * Read data from the stream. - * - * \param int $count - * Requested read count. This value must be large enough - * to hold the whole message digest. - * - * \retval bool - * \b false is returned on error (eg. when the requested - * read count is too small). - * - * \retval string - * Message digest for the data fed to the data processor - * so far, in raw (binary) form. - */ - public function stream_read($count) - { - // @codingStandardsIgnoreEnd - - $clone = clone $this->implementation; - $result = $clone->finalize(true); - return strlen($result) > $count ? false : $result; - } - - // @codingStandardsIgnoreStart - /** - * Push data into the stream wrapper. - * - * \param string $data - * Data to add to the stream's buffer. - * - * \retval int - * Size of the data that's effectively been added - * to the buffer. This may be less (even zero) - * than the length of the given data. - */ - public function stream_write($data) - { - // @codingStandardsIgnoreEnd - - $this->implementation->update($data); - return strlen($data); - } -} diff --git a/src/Cryptal/Streams/Mac.php b/src/Cryptal/Streams/Mac.php deleted file mode 100644 index 6474608..0000000 --- a/src/Cryptal/Streams/Mac.php +++ /dev/null @@ -1,189 +0,0 @@ -context) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Missing cryptographic context', E_USER_ERROR); - } - return false; - } - $ctxOptions = stream_context_get_options($this->context); - - if (false === strpos($mode, '+')) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Invalid mode', E_USER_ERROR); - } - return false; - } - - if (!isset($ctxOptions['cryptal']['key'])) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Invalid cryptographic context', E_USER_ERROR); - } - return false; - } - - $parts = parse_url($path); - if ($parts === false || !isset($parts['host'], $parts['path'])) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Invalid path', E_USER_ERROR); - } - return false; - } - - if (!strncasecmp($parts['host'], 'MAC_', 4)) { - $algo = $parts['host']; - } else { - $algo = 'MAC_' . strtoupper($parts['host']); - } - - $algo = '\\fpoirotte\\Cryptal\\Implementers\\Mac::' . $algo; - if (!defined($algo)) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Invalid MAC algorithm', E_USER_ERROR); - } - return false; - } - - if (!strncasecmp($parts['path'], '/HASH_', 6)) { - $hash = '\\fpoirotte\\Cryptal\\Implementers\\Hash::' . substr($parts['path'], 1); - if (!defined($hash)) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Invalid hashing algorithm', E_USER_ERROR); - } - return false; - } - - $sub = new \fpoirotte\Cryptal\Implementers\Hash(constant($hash)); - } elseif (!strncasecmp($parts['path'], '/CIPHER_', 8)) { - $cipher = '\\fpoirotte\\Cryptal\\Implementers\\Crypto::' . substr($parts['path'], 1); - if (!defined($cipher)) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Invalid cipher algorithm', E_USER_ERROR); - } - return false; - } - - $sub = new \fpoirotte\Cryptal\Implementers\Crypto( - constant($cipher), - CryptoInterface::MODE_ECB, - new \fpoirotte\Cryptal\Padding\None() - ); - } - - $allowUnsafe = isset($ctxOptions['cryptal']['allowUnsafe']) ? - (bool) $ctxOptions['cryptal']['allowUnsafe'] : false; - try { - $this->implementation = new \fpoirotte\Cryptal\Implementers\Mac( - constant($algo), - $sub, - $ctxOptions['cryptal']['key'] - ); - } catch (\Exception $e) { - if (self::DEBUG || $options & STREAM_REPORT_ERRORS) { - trigger_error('Could not create data processor: ' . $e, E_USER_WARNING); - } - return false; - } - - $opened_path = $path; - return true; - } - - // @codingStandardsIgnoreStart - /** - * Read data from the stream. - * - * \param int $count - * Requested read count. This value must be large enough - * to hold the whole Message Authentication Code. - * - * \retval bool - * \b false is returned on error (eg. when the requested - * read count is too small). - * - * \retval string - * Authentication Code for the message fed to the data processor - * so far, in raw (binary) form. - */ - public function stream_read($count) - { - // @codingStandardsIgnoreEnd - - $clone = clone $this->implementation; - $result = $clone->finalize(true); - return strlen($result) > $count ? false : $result; - } - - // @codingStandardsIgnoreStart - /** - * Push data into the stream wrapper. - * - * \param string $data - * Data to add to the stream's buffer. - * - * \retval int - * Size of the data that's effectively been added - * to the buffer. This may be less (even zero) - * than the length of the given data. - */ - public function stream_write($data) - { - // @codingStandardsIgnoreEnd - - $this->implementation->update($data); - return strlen($data); - } -} diff --git a/tests/API/Filters/BinifyTest.php b/tests/API/Filters/BinifyTest.php new file mode 100644 index 0000000..097a038 --- /dev/null +++ b/tests/API/Filters/BinifyTest.php @@ -0,0 +1,53 @@ +assertSame($expected, stream_get_contents($stream)); + } + + /** + * @expectedException RuntimeException + * @expectedExceptionMessage Invalid data in input + */ + public function testInvalidByte() + { + $input = "ZZ"; + $stream = fopen("php://memory", "w+b"); + stream_filter_append($stream, 'cryptal.binify', STREAM_FILTER_READ); + fwrite($stream, $input); + fseek($stream, 0, SEEK_SET); + $this->assertSame('foo', stream_get_contents($stream)); + } + + /** + * @expectedException RuntimeException + * @expectedExceptionMessage Odd number of bytes in input + */ + public function testOddNumberOfButes() + { + $input = "303"; + $stream = fopen("php://memory", "w+b"); + stream_filter_append($stream, 'cryptal.binify', STREAM_FILTER_READ); + fwrite($stream, $input); + fseek($stream, 0, SEEK_SET); + $this->assertSame('0', stream_get_contents($stream)); + } +} diff --git a/tests/API/Filters/CryptoTest.php b/tests/API/Filters/CryptoTest.php new file mode 100644 index 0000000..1c59ed2 --- /dev/null +++ b/tests/API/Filters/CryptoTest.php @@ -0,0 +1,140 @@ + array( + ModeEnum::MODE_CBC(), + $plaintext, + $key, + '000102030405060708090a0b0c0d0e0f', + '7649abac8119b246cee98e9b12e9197d' . + '5086cb9b507219ee95db113a917678b2' . + '73bed6b8e3c1743b7116e69e22229516' . + '3ff1caa1681fac09120eca307586e1a7', + '', + '', + ), + 'aes-128-cfb' => array( + ModeEnum::MODE_CFB(), + $plaintext, + $key, + '000102030405060708090a0b0c0d0e0f', + '3b3fd92eb72dad20333449f8e83cfb4a' . + 'c8a64537a0b3a93fcde3cdad9f1ce58b' . + '26751f67a3cbb140b1808cf187a4f4df' . + 'c04b05357c5d1c0eeac4c66f9ff7f2e6', + '', + '', + ), + 'aes-128-ctr' => array( + ModeEnum::MODE_CTR(), + $plaintext, + $key, + 'f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff', + '874d6191b620e3261bef6864990db6ce' . + '9806f66b7970fdff8617187bb9fffdff' . + '5ae4df3edbd5d35e5b4f09020db03eab' . + '1e031dda2fbe03d1792170a0f3009cee', + '', + '', + ), + 'aes-128-ecb' => array( + ModeEnum::MODE_ECB(), + $plaintext, + $key, + '000102030405060708090a0b0c0d0e0f', + '3ad77bb40d7a3660a89ecaf32466ef97' . + 'f5d3d58503b9699de785895a96fdbaaf' . + '43b1cd7f598ece23881b00e3ed030688' . + '7b0c785e27e8ad3f8223207104725dd4', + '', + '', + ), + 'aes-128-ofb' => array( + ModeEnum::MODE_OFB(), + $plaintext, + $key, + '000102030405060708090a0b0c0d0e0f', + '3b3fd92eb72dad20333449f8e83cfb4a' . + '7789508d16918f03f53c52dac54ed825' . + '9740051e9c5fecf64344f7a82260edcc' . + '304c6528f659c77866a510d9c1d6ae5e', + '', + '', + ), + ); + } + + /** + * @dataProvider vectors + */ + public function testFilterFor($mode, $plaintext, $key, $iv, $ciphertext, $aad, $tag) + { + $iv = pack('H*', $iv); + $key = pack('H*', $key); + + // Test the encryption + $stream = fopen("php://memory", "w+b"); + stream_filter_append( + $stream, + 'cryptal.encrypt', + STREAM_FILTER_READ, + array( + // We use the default padding scheme (None) + // and tag length (128 bits). + 'mode' => $mode, + 'algorithm' => CipherEnum::CIPHER_AES_128(), + 'iv' => $iv, + 'key' => $key, + + // We're using the stub which is based on PHP code + 'allowUnsafe' => true, + ) + ); + fwrite($stream, pack('H*', $plaintext)); + fseek($stream, 0, SEEK_SET); + $this->assertSame($ciphertext, bin2hex(stream_get_contents($stream))); + + // And decryption too + $stream = fopen("php://memory", "w+b"); + stream_filter_append( + $stream, + 'cryptal.decrypt', + STREAM_FILTER_READ, + array( + // We use the default padding scheme (None) + // and tag length (128 bits). + 'mode' => $mode, + 'algorithm' => CipherEnum::CIPHER_AES_128(), + 'iv' => $iv, + 'key' => $key, + + // We're using the AES stub, which is based on PHP code + 'allowUnsafe' => true, + ) + ); + fwrite($stream, pack('H*', $ciphertext)); + fseek($stream, 0, SEEK_SET); + $this->assertSame($plaintext, bin2hex(stream_get_contents($stream))); + } +} diff --git a/tests/API/Filters/HashTest.php b/tests/API/Filters/HashTest.php new file mode 100644 index 0000000..206a4a4 --- /dev/null +++ b/tests/API/Filters/HashTest.php @@ -0,0 +1,38 @@ + array(HashEnum::HASH_MD5(), $data, 'c897d1410af8f2c74fba11b1db511e9e'), + // Test vector generated using sha1sum + 'SHA1' => array(HashEnum::HASH_SHA1(), $data, 'f951b101989b2c3b7471710b4e78fc4dbdfa0ca6'), + ); + } + + /** + * @dataProvider vectors + */ + public function testFilterFor($algorithm, $data, $expected) + { + $stream = fopen("php://memory", "w+b"); + stream_filter_append($stream, 'cryptal.hash', STREAM_FILTER_READ, array('algorithm' => $algorithm)); + fwrite($stream, $data); + fseek($stream, 0, SEEK_SET); + $this->assertSame($expected, bin2hex(stream_get_contents($stream))); + } +} diff --git a/tests/API/Filters/HexifyTest.php b/tests/API/Filters/HexifyTest.php new file mode 100644 index 0000000..8de3815 --- /dev/null +++ b/tests/API/Filters/HexifyTest.php @@ -0,0 +1,25 @@ +assertSame($expected, stream_get_contents($stream)); + } +} diff --git a/tests/API/Filters/MacTest.php b/tests/API/Filters/MacTest.php new file mode 100644 index 0000000..5bc66b9 --- /dev/null +++ b/tests/API/Filters/MacTest.php @@ -0,0 +1,34 @@ + MacEnum::MAC_CMAC(), + 'innerAlgorithm' => CipherEnum::CIPHER_AES_128(), + 'key' => pack('H*', '0f0e0d0c0b0a09080706050403020100'), + + // Since both the CMAC implementation and the AES stub + // are based on userland PHP code, this must be set to true. + 'allowUnsafe' => true, + ) + ); + fwrite($stream, "hello world!\n"); + fseek($stream, 0, SEEK_SET); + $this->assertSame($expected, bin2hex(stream_get_contents($stream))); + } +} diff --git a/tests/API/Misc/PaddingTest.php b/tests/API/Misc/PaddingTest.php index c719f1e..33fa380 100644 --- a/tests/API/Misc/PaddingTest.php +++ b/tests/API/Misc/PaddingTest.php @@ -71,7 +71,7 @@ public function paddingProvider() /** * @dataProvider paddingProvider */ - public function testPadding($bufferSize, $scheme, $expected) + public function testPaddingWithScheme($bufferSize, $scheme, $expected) { $this->assertSame( bin2hex($expected), diff --git a/tests/AesBasedTestCase.php b/tests/AesBasedTestCase.php index 3f96438..29e3fb8 100644 --- a/tests/AesBasedTestCase.php +++ b/tests/AesBasedTestCase.php @@ -5,6 +5,8 @@ use fpoirotte\Cryptal\Padding\None; use fpoirotte\Cryptal\ModeEnum; use fpoirotte\Cryptal\CipherEnum; +use fpoirotte\Cryptal\ImplementationTypeEnum; +use fpoirotte\Cryptal\Registry; abstract class AesBasedTestCase extends \PHPUnit\Framework\TestCase { @@ -13,6 +15,24 @@ abstract class AesBasedTestCase extends \PHPUnit\Framework\TestCase public static function setUpBeforeClass() { self::$cache = array(); + + // Initialize the library. + \fpoirotte\Cryptal::init(); + + $registry = Registry::getInstance(); + $registry->reset()->registerDefaultAlgorithms()->addCipher( + '', + '\\fpoirotte\\Cryptal\\Tests\\AesEcbStub', + CipherEnum::CIPHER_AES_128(), + ModeEnum::MODE_ECB(), + ImplementationTypeEnum::TYPE_USERLAND() + ); + } + + public static function tearDownAfterClass() + { + $registry = Registry::getInstance(); + $registry->reset()->load(true); } public function getCipher($key) diff --git a/tests/AesEcbStub.php b/tests/AesEcbStub.php index 54d4195..3cc0dab 100644 --- a/tests/AesEcbStub.php +++ b/tests/AesEcbStub.php @@ -9,6 +9,10 @@ use fpoirotte\Cryptal\CipherEnum; use fpoirotte\Cryptal\ModeEnum; +/** + * See http://testprotect.com/appendix/AEScalc for an easy way + * to get the values online. + */ class AesEcbStub implements CryptoInterface { protected $map; diff --git a/tests/Implementation/CryptoStreamTest.php b/tests/Implementation/CryptoStreamTest.php deleted file mode 100644 index 11a79ae..0000000 --- a/tests/Implementation/CryptoStreamTest.php +++ /dev/null @@ -1,118 +0,0 @@ -markTestSkipped("Could not find a valid AES implementation: $e"); - } - } - - public function provider() - { - // We use AES-128 to test the stream wrapper because it is assumed - // most (all?) implementations support it. - // - // These test vectors come from appendix F of - // http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf - // These were preferred over other accessible test vectors - // because they all use the same key & multi-block plaintext - // (and most of them share the same IV as well). - return array( - 'aes-128-cbc' => array( - 'cbc', - '000102030405060708090a0b0c0d0e0f', - '7649abac8119b246cee98e9b12e9197d' . - '5086cb9b507219ee95db113a917678b2' . - '73bed6b8e3c1743b7116e69e22229516' . - '3ff1caa1681fac09120eca307586e1a7' - ), - 'aes-128-cfb' => array( - 'cfb', - '000102030405060708090a0b0c0d0e0f', - '3b3fd92eb72dad20333449f8e83cfb4a' . - 'c8a64537a0b3a93fcde3cdad9f1ce58b' . - '26751f67a3cbb140b1808cf187a4f4df' . - 'c04b05357c5d1c0eeac4c66f9ff7f2e6' - ), - 'aes-128-ctr' => array( - 'ctr', - 'f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff', - '874d6191b620e3261bef6864990db6ce' . - '9806f66b7970fdff8617187bb9fffdff' . - '5ae4df3edbd5d35e5b4f09020db03eab' . - '1e031dda2fbe03d1792170a0f3009cee' - ), - 'aes-128-ecb' => array( - 'ecb', - '000102030405060708090a0b0c0d0e0f', - '3ad77bb40d7a3660a89ecaf32466ef97' . - 'f5d3d58503b9699de785895a96fdbaaf' . - '43b1cd7f598ece23881b00e3ed030688' . - '7b0c785e27e8ad3f8223207104725dd4' - ), - 'aes-128-ofb' => array( - 'ofb', - '000102030405060708090a0b0c0d0e0f', - '3b3fd92eb72dad20333449f8e83cfb4a' . - '7789508d16918f03f53c52dac54ed825' . - '9740051e9c5fecf64344f7a82260edcc' . - '304c6528f659c77866a510d9c1d6ae5e' - ), - ); - } - - /** - * @dataProvider provider - */ - public function testStreamingInterfaceWith($mode, $iv, $expected) - { - $plaintext = '6bc1bee22e409f96e93d7e117393172a' . - 'ae2d8a571e03ac9c9eb76fac45af8e51' . - '30c81c46a35ce411e5fbc1191a0a52ef' . - 'f69f2445df4f9b17ad2b417be66c3710'; - - $ctx = stream_context_create( - array( - 'cryptal' => array( - 'key' => pack('H*', '2b7e151628aed2a6abf7158809cf4f3c'), - 'IV' => pack('H*', $iv), - ) - ) - ); - - $encrypt = fopen("cryptal.encrypt://$mode/aes_128", 'w+', false, $ctx); - fwrite($encrypt, pack('H*', $plaintext)); - fflush($encrypt); - - $ciphertext = ''; - while ($data = fread($encrypt, 1024)) { - $ciphertext .= $data; - } - $this->assertSame($expected, bin2hex($ciphertext)); - - $decrypt = fopen("cryptal.decrypt://$mode/aes_128", 'w+', false, $ctx); - fwrite($decrypt, $ciphertext); - fflush($decrypt); - $plaintext2 = ''; - while ($data = fread($decrypt, 1024)) { - $plaintext2 .= $data; - } - $this->assertSame($plaintext, bin2hex($plaintext2)); - } -} diff --git a/tests/aes_ecb/0f0e0d0c0b0a09080706050403020100.dat b/tests/aes_ecb/0f0e0d0c0b0a09080706050403020100.dat index 874a399..3d6868a 100644 --- a/tests/aes_ecb/0f0e0d0c0b0a09080706050403020100.dat +++ b/tests/aes_ecb/0f0e0d0c0b0a09080706050403020100.dat @@ -14,3 +14,4 @@ c0000001bbaa99887766554433221100 6356550c3a91cb3c9448ebdef58dd87e 67fc40cbe723a65495889caedb28beff a873afb6c0e386b22d83cdbf928f4864 92dc41f962ba882dee780b4acdcfd7ed cdb26a360de24d4d6ff7e7e779429bb7 688f55897597cec6b9bb00a4c82b2960 dfe6b2385218eaa64d637d79cd865d3c +fca120ea291096d6fdca53dd2169ddbd a9a5079a7e416683be1e24ddca8d22a2 diff --git a/tests/aes_ecb/2b7e151628aed2a6abf7158809cf4f3c.dat b/tests/aes_ecb/2b7e151628aed2a6abf7158809cf4f3c.dat index c7b9600..e1da18a 100644 --- a/tests/aes_ecb/2b7e151628aed2a6abf7158809cf4f3c.dat +++ b/tests/aes_ecb/2b7e151628aed2a6abf7158809cf4f3c.dat @@ -6,3 +6,23 @@ 765d7109f920644f5171246215478c72 dfa66747de9ae63030ca32611497c827 8180dd3993c20283cd812465eb208fa6 c93d11bfaf08c5dc4d90b37b4dee002b c44ce3e245366dad9c3e128fd9b49fe5 51f0bebf7e3b9d92fc49741779363cfe +6bc0bce12a459991e134741a7f9e1925 7649abac8119b246cee98e9b12e9197d +000102030405060708090a0b0c0d0e0f 50fe67cc996d32b6da0937e99bafec60 +50fe67cc996d32b6da0937e99bafec60 d9a4dada0892239f6b8b3d7680e15674 +d9a4dada0892239f6b8b3d7680e15674 a78819583f0308e7a6bf36b1386abf23 +a78819583f0308e7a6bf36b1386abf23 c6d3416d29165c6fcb8e51a227ba994e +ae2d8a571e03ac9c9eb76fac45af8e51 f5d3d58503b9699de785895a96fdbaaf +30c81c46a35ce411e5fbc1191a0a52ef 43b1cd7f598ece23881b00e3ed030688 +f69f2445df4f9b17ad2b417be66c3710 7b0c785e27e8ad3f8223207104725dd4 +f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff ec8cdf7398607cb0f2d21675ea9ea1e4 +f0f1f2f3f4f5f6f7f8f9fafbfcfdff00 362b7c3c6773516318a077d7fc5073ae +f0f1f2f3f4f5f6f7f8f9fafbfcfdff01 6a2cc3787889374fbeb4c81b17ba6c44 +f0f1f2f3f4f5f6f7f8f9fafbfcfdff02 e89c399ff0f198c6d40a31db156cabfe +3b3fd92eb72dad20333449f8e83cfb4a 668bcf60beb005a35354a201dab36bda +d86421fb9f1a1eda505ee1375746972c 5086cb9b507219ee95db113a917678b2 +c8a64537a0b3a93fcde3cdad9f1ce58b 16bd032100975551547b4de89daea630 +604ed7ddf32efdff7020d0238b7c2a5d 73bed6b8e3c1743b7116e69e22229516 +26751f67a3cbb140b1808cf187a4f4df 36d42170a312871947ef8714799bc5f6 +8521f2fd3c8eef2cdc3da7e5c44ea206 3ff1caa1681fac09120eca307586e1a7 +7648a9af851cb441c6e084901ee41772 46e335b8ea11bcc5b4eb7f49d114ff43 +f2713e83adc99f5f657d075ebb7a0a1c 3f293630cf20bb285320f3ef80f185ed