From bc2d6b384e0b5fbdbc8781d94f0f0af52639d6f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Poirotte?= Date: Sun, 30 Jul 2017 19:21:02 +0200 Subject: [PATCH] Major rewrite using filters instead of streams This change should be transparent for the plugins as the rest of the API has not changed and the tests have already been converted to use the filters. --- docs/src/Introduction.rst | 2 +- docs/src/Usage.rst | 394 +++++++++++++----- phpunit.xml | 3 + src/Cryptal.php | 18 +- src/Cryptal/Filters/Binify.php | 52 +++ src/Cryptal/Filters/Crypto.php | 143 +++++++ src/Cryptal/Filters/Hash.php | 39 ++ src/Cryptal/Filters/Hexify.php | 16 + src/Cryptal/Filters/Mac.php | 63 +++ src/Cryptal/Modes/CCM.php | 10 +- src/Cryptal/Modes/CTR.php | 10 +- src/Cryptal/Registry.php | 152 ++++--- src/Cryptal/Streams/Crypto.php | 322 -------------- src/Cryptal/Streams/Hash.php | 143 ------- src/Cryptal/Streams/Mac.php | 189 --------- tests/API/Filters/BinifyTest.php | 53 +++ tests/API/Filters/CryptoTest.php | 140 +++++++ tests/API/Filters/HashTest.php | 38 ++ tests/API/Filters/HexifyTest.php | 25 ++ tests/API/Filters/MacTest.php | 34 ++ tests/API/Misc/PaddingTest.php | 2 +- tests/AesBasedTestCase.php | 20 + tests/AesEcbStub.php | 4 + tests/Implementation/CryptoStreamTest.php | 118 ------ .../0f0e0d0c0b0a09080706050403020100.dat | 1 + .../2b7e151628aed2a6abf7158809cf4f3c.dat | 20 + 26 files changed, 1038 insertions(+), 973 deletions(-) create mode 100644 src/Cryptal/Filters/Binify.php create mode 100644 src/Cryptal/Filters/Crypto.php create mode 100644 src/Cryptal/Filters/Hash.php create mode 100644 src/Cryptal/Filters/Hexify.php create mode 100644 src/Cryptal/Filters/Mac.php delete mode 100644 src/Cryptal/Streams/Crypto.php delete mode 100644 src/Cryptal/Streams/Hash.php delete mode 100644 src/Cryptal/Streams/Mac.php create mode 100644 tests/API/Filters/BinifyTest.php create mode 100644 tests/API/Filters/CryptoTest.php create mode 100644 tests/API/Filters/HashTest.php create mode 100644 tests/API/Filters/HexifyTest.php create mode 100644 tests/API/Filters/MacTest.php delete mode 100644 tests/Implementation/CryptoStreamTest.php 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