├── .travis.yml ├── ChangeLog.md ├── README.md ├── composer.json ├── src └── Secret.php └── tests ├── SecretTest.php ├── bootstrap.php └── phpunit.xml /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - 7 8 | 9 | sudo: false 10 | 11 | matrix: 12 | allow_failures: 13 | - php: 7 14 | 15 | script: phpunit --coverage-text --configuration tests/phpunit.xml -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | 0.3.0 2 | ----- 3 | 4 | - Drop MCrypt support. 5 | - Drop extension availability checks. 6 | - Use MB_OVERLOAD_STRING constant to test for mbstring.func_override. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Simple Encryption for PHP 4 | ========================= 5 | 6 | A PHP library for symmetric encryption, making it easy, safe and accessible for everybody. 7 | 8 | **EXPERIMENTAL! DO NOT use this library until the 1.0 version is tagged!** 9 | 10 | [![Build Status](https://travis-ci.org/narfbg/SimpleEncryption.svg?branch=master)](https://travis-ci.org/narfbg/SimpleEncryption) 11 | 12 | Introduction 13 | ------------ 14 | 15 | Everybody wants to do encryption, for one reason or another. The problem is, very few people know enough about cryptography to implement it properly. It might seem easy, or trivial, but for your own good, trust me when I say this: IT'S NOT! 16 | 17 | Most people don't even know the difference between encryption and hashing, and there's a good reason why cryptography is a science subject in its own right. Being a good, experienced, even exceptional developer is often not enough. MCrypt alone is not enough. 18 | 19 | That is why cryptography experts will tell you to never write your own crypto code and always to use well-vetted, time-tested libraries that will *make all the choices for you*. Taking a choice away from you might not sound good at first, but really, it is. 20 | There are so many choices to be made and so many wrong ones in particular, that chances are, you're not even aware of all of them, let alone qualified to make them. 21 | 22 | In the PHP world, there's another, rather large problem - there are few cryptography libraries that do everything right *(and a lot more that don't)*, and I've never seen one that is easy to use. 23 | Even with the good ones, it's really easy to screw up. 24 | 25 | *SimpleEncryption* is an attempt to solve all of this. 26 | 27 | *Note: The library is well-covered with unit tests, but not audited yet. I'm hoping that crypto experts within the OSS community will do the latter.* 28 | 29 | ### Technical details 30 | 31 | If you must know, this is what *SimpleEncryption* utilizes: 32 | 33 | - AES-256-CTR for encryption (yes, the IV is always random) 34 | - HMAC SHA-256 for authentication (encrypt, then HMAC; safe from timing attacks) 35 | - HKDF for key derivation (one key for encryption, one for authentication) 36 | 37 | Requirements 38 | ------------ 39 | 40 | - PHP 5.4 41 | - OpenSSL extension 42 | 43 | Installation and loading 44 | ------------------------ 45 | 46 | TODO: Link to downloads & packagist, once published. 47 | 48 | Then of course, you'll need to link to the library in your own code, either by using the [PSR-4](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md)-compliant autoloader, or manually, like this: 49 | 50 | require_once('path/to/SimpleEncryption/src/Secret.php'); 51 | 52 | And finally, import it into your own namespace: 53 | 54 | use \Narf\SimpleEncryption\Secret; 55 | 56 | Usage 57 | ----- 58 | 59 | ### Encrypting data 60 | 61 | All you need to to is to just create a `Secret` object with your confidential data and then call its `getCipherText()` method. The library will automatically create an encryption key, which you can get via the `getKey()` method: 62 | 63 | $mySecret = new Secret('My secret message!'); 64 | 65 | $encryptedData = $mySecret->getCipherText(); 66 | $key = $mySecret->getKey(); 67 | 68 | ### Decrypting data 69 | 70 | Decrypting data is just as easy, simply create a `Secret` object with the previously encrypted data and the encryption key, then call the `getPlainText()` method to do the actual decryption: 71 | 72 | $mySecret = new Secret($encryptedData, $key); 73 | echo $mySecret->getPlainText(); 74 | 75 | ### Creating and using your own keys 76 | 77 | While having different encryption keys for each piece of encrypted data is always the safe bet, this is not always practical. Therefore, sometimes you'll need to pass your own key to the `Secret` class before encrypting data. 78 | 79 | Before showing how to actually use your own keys however, it's important to note that an encryption key MUST NOT be just a password, nor the output of a hashing function. It MUST NOT be anything that is readable as standard ASCII. If you need to create your own key, use the `Secret::getRandomBytes()` method: 80 | 81 | $yourKey = Secret::getRandomBytes(32); 82 | 83 | (the length has to be 32 bytes, or 64 when hex-encoded, but the library will not let you pass a key with a different size anyway) 84 | 85 | Now, after you have a key, in order to encrypt data with it, you'll have to pass it to the `Secret` class *before* encryption. 86 | However, since creating a `Secret` object with a key would usually mean that you're providing it with already encrypted data, you'll have to manually tell it what the input type is. 87 | 88 | This is done by passing one of `Secret::PLAINTEXT` or `Secret::ENCRYPTED` as the third parameter: 89 | 90 | $yourSecret = new Secret('Your secret message', $yourKey, Secret::PLAINTEXT); 91 | $encryptedData = $yourSecret->getCipherText(); 92 | 93 | For convenience, `Secret::ENCRYPTED` is also accepted, although it's not functionally required: 94 | 95 | $yourSecret = new Secret($encryptedData, $yourKey, Secret::ENCRYPTED); 96 | $plaintextData = $yourSecret->getPlainText(); 97 | 98 | ### Error handling 99 | 100 | In case of an error, such as missing a CSPRNG source or failed authentication, the Secret class will throw a `RuntimeException`. 101 | Therefore, in order to avoid leaking sensitive data, you'll need to catch such exceptions: 102 | 103 | try 104 | { 105 | $secret = new Secret($encryptedData, $encryptionKey); 106 | $plainText = $secret->getCipherText(); 107 | } 108 | catch (\RuntimeException $e) 109 | { 110 | // Handle the error 111 | } 112 | 113 | Class reference 114 | --------------- 115 | 116 | - **void __construct($inputText[, $masterKey = null[, $inputType = null]])** 117 | **$inputText**: The input data 118 | **$masterKey**: Hex-encoded encryption key 119 | **$inputType**: One of `null`, `Secret::PLAINTEXT` or `Secret::ENCRYPTED` 120 | 121 | If `$inputType` is not provided, then providing an encryption key means that `$inputText` is encrypted data, and vice-versa, not providing a key means that `$inputText` is a plain-text. 122 | 123 | - **string getCipherText()** 124 | 125 | Encrypts (anew) and returns the data, generating a key in the process, if necessary. 126 | 127 | - **string getPlainText()** 128 | 129 | Decrypts (if necessary) and returns the plain-text version of the secret data. 130 | 131 | - **string getKey()** 132 | 133 | Returns the hex-encoded encryption key, regardless if it was pre-set or if it is a newly generated one. 134 | 135 | - **string static getRandomBytes($length[, $rawOutput = false])** 136 | **$length**: Output length (binary size) 137 | **$rawOutput**: Whether to return raw binary data or a hex-encoded string 138 | 139 | Returns a stream of randomly generated bytes, suitable for creating encryption keys. 140 | 141 | - **string static hkdf($key, $digest[, $length = null[, info = ''[, $salt = null]]])** 142 | **$key**: Input key material (binary) 143 | **$digest**: HMAC digest (algorithm) 144 | **$length**: Output length 145 | **$info**: Application/context specific information 146 | **$salt**: Salt 147 | 148 | An [RFC 5869](https://tools.ietf.org/rfc/rfc5869.txt)-compatible HKDF implementation. Used internally by the library and exposed because there's no reason not to. If you don't know what it is, you don't need it. 149 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "narf/simple-encryption", 3 | "type": "library", 4 | "version": "0.3.0", 5 | "description": "A simple library for symmetric encryption under PHP", 6 | "keywords": ["encryption", "crypto", "encrypt", "aes", "ctr", "hmac", "hkdf", "openssl"], 7 | "homepage": "https://github.com/narfbg/SimpleEncryption", 8 | "license": "ISC", 9 | "authors": [ 10 | { 11 | "name": "Andrey Andreev", 12 | "email": "narf@devilix.net", 13 | "role": "Developer" 14 | } 15 | ], 16 | "require": { 17 | "php": ">= 5.4", 18 | "ext-openssl": "*" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Narf\\SimpleEncryption\\": "src/" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Secret.php: -------------------------------------------------------------------------------- 1 | 4 | * All rights reserved. 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | /** 20 | * Simple Encryption for PHP 21 | * 22 | * A simple symmetric encryption library, currently providing 23 | * AES-256-CTR-HMAC-SHA256 as the only available encryption method. 24 | * 25 | * @package SimpleEncryption 26 | * @author Andrey Andreev 27 | * @copyright Copyright (c) 2014, Andrey Andreev 28 | * @license http://opensource.org/licenses/ISC ISC License (ISC) 29 | * @link https://github.com/narfbg/SimpleEncryption 30 | */ 31 | namespace Narf\SimpleEncryption; 32 | 33 | class Secret { 34 | 35 | const VERSION = '0.3.0'; 36 | 37 | // These are passed to the constructor to specify the 38 | // input data type. In the future, when the default 39 | // encryption scheme changes, the ENCRYPTED value will 40 | // change as well, and another constant will be added 41 | // for (decryption) backwards compatibility. 42 | const PLAINTEXT = 0; 43 | const ENCRYPTED = 1; 44 | 45 | // Data placeholders 46 | private $inputText, $inputType, $masterKey; 47 | 48 | /** 49 | * __construct() 50 | * 51 | * @param string $inputText Input text 52 | * @param int $inputType Input type 53 | * @param string $masterKey Master key 54 | */ 55 | public function __construct($inputText, $masterKey = null, $inputType = null) 56 | { 57 | // Validate input type 58 | if (isset($inputType)) 59 | { 60 | if ($inputType === self::ENCRYPTED && ! isset($masterKey)) 61 | { 62 | throw new \InvalidArgumentException('Input type is Secret::ENCRYPTED, but there is no key.'); 63 | } 64 | elseif ($inputType !== self::PLAINTEXT && $inputType !== self::ENCRYPTED) 65 | { 66 | throw new \InvalidArgumentException('Input type must be Secret::PLAINTEXT or Secret::ENCRYPTED'); 67 | } 68 | 69 | $this->inputType = $inputType; 70 | } 71 | 72 | // Validate key (length) if it exists, and guess the input type if necessary 73 | if (isset($masterKey)) 74 | { 75 | if ( ! \preg_match('/^[0-9a-f]{64}$/i', $masterKey)) 76 | { 77 | throw new \InvalidArgumentException('Invalid key format, please use getKey() to create your own keys.'); 78 | } 79 | 80 | $this->masterKey = \pack('H*', $masterKey); 81 | isset($this->inputType) OR $this->inputType = self::ENCRYPTED; 82 | } 83 | elseif ( ! isset($this->inputType)) $this->inputType = self::PLAINTEXT; 84 | 85 | $this->inputText = $inputText; 86 | } 87 | 88 | /** 89 | * getCipherText() 90 | * 91 | * Does the following: 92 | * 93 | * - If the input was an encrypted message, calls getPlainText() to decrypt it 94 | * - If the input was a plain-text message and no master key is set, the key is generated 95 | * - Generates a random IV 96 | * - Derives a cipher and a HMAC key from the master key, via HKDF 97 | * - Encrypts the plainText message and prepends the IV to it 98 | * - Prepends a HMAC-SHA256 message to the cipher text encodes it using Base64 99 | * 100 | * The result is not cached and the whole process is repeated for each call, 101 | * resulting in different IV and cipher text every time. 102 | * 103 | * @return string Cipher text 104 | */ 105 | public function getCipherText() 106 | { 107 | if (isset($this->masterKey)) $iv = self::getRandomBytes(16, true); 108 | else list($this->masterKey, $iv) = \str_split(self::getRandomBytes(48, true), 32); 109 | 110 | list($cipherKey, $hmacKey) = \str_split(self::hkdf($this->masterKey, 'sha512', 64, 'aes-256-ctr-hmac-sha256'), 32); 111 | 112 | $data = ($this->inputType === self::PLAINTEXT) 113 | ? $this->inputText 114 | : $this->getPlainText(); 115 | 116 | if (($data = \openssl_encrypt($data, 'aes-256-ctr', $cipherKey, 1, $iv)) === false) 117 | { 118 | // @codeCoverageIgnoreStart 119 | throw new \RuntimeException('Error during encryption procedure.'); 120 | // @codeCoverageIgnoreEnd 121 | } 122 | 123 | return \base64_encode(\hash_hmac('sha256', $iv.$data, $hmacKey, true).$iv.$data); 124 | } 125 | 126 | /** 127 | * getPlainText() 128 | * 129 | * Does the following: 130 | * 131 | * - If the input was a plain-text message, simply returns it 132 | * - The cipher and HMAC keys are derived from the master key 133 | * - Validates and strips Base64 encoding 134 | * - Calls authenticate(), which strips the Base64 encoding and HMAC message 135 | * - Separates the IV and decrypts the message 136 | * 137 | * The result is cached to speed-up subsequent calls. 138 | * 139 | * @return string Plain-text message 140 | */ 141 | public function getPlainText() 142 | { 143 | if ($this->inputType === self::PLAINTEXT) 144 | { 145 | return $this->inputText; 146 | } 147 | 148 | list($cipherKey, $hmacKey) = \str_split(self::hkdf($this->masterKey, 'sha512', 64, 'aes-256-ctr-hmac-sha256'), 32); 149 | 150 | // authenticate() receives $data by reference 151 | $data = $this->inputText; 152 | $this->authenticate($data, $hmacKey); 153 | 154 | $data = \openssl_decrypt( 155 | self::substr($data, 16), 156 | 'aes-256-ctr', 157 | $cipherKey, 158 | 1, 159 | self::substr($data, 0, 16) 160 | ); 161 | 162 | if ($data === false) 163 | { 164 | // @codeCoverageIgnoreStart 165 | throw new \RuntimeException('Error during decryption procedure.'); 166 | // @codeCoverageIgnoreEnd 167 | } 168 | 169 | return $data; 170 | } 171 | 172 | /** 173 | * getKey() 174 | * 175 | * Generates a key, unless already set, and then returns it. 176 | * 177 | * @return string Key 178 | */ 179 | public function getKey() 180 | { 181 | isset($this->masterKey) OR $this->masterKey = self::getRandomBytes(32, true); 182 | return \bin2hex($this->masterKey); 183 | } 184 | 185 | /** 186 | * getRandomBytes() 187 | * 188 | * Reads the specified amount of data from the system's PRNG. 189 | * 190 | * @param int $length Desired output length 191 | * @return string A pseudo-random stream of bytes 192 | */ 193 | public static function getRandomBytes($length, $rawOutput = false) 194 | { 195 | if ( ! is_int($length) OR $length < 1) 196 | { 197 | throw new \InvalidArgumentException('Length must be an integer larger than 0.'); 198 | } 199 | 200 | // @codeCoverageIgnoreStart 201 | if (\function_exists('openssl_random_pseudo_bytes')) 202 | { 203 | $cryptoStrong = null; 204 | if (($output = \openssl_random_pseudo_bytes($length, $cryptoStrong)) !== false && $cryptoStrong) 205 | { 206 | return ($rawOutput) ? $output : \bin2hex($output); 207 | } 208 | } 209 | if (\defined('MCRYPT_DEV_URANDOM')) 210 | { 211 | if (($output = \mcrypt_create_iv($length, MCRYPT_DEV_URANDOM)) !== false) 212 | { 213 | return ($rawOutput) ? $output : \bin2hex($output); 214 | } 215 | } 216 | 217 | if (\is_readable('/dev/urandom') && ($fp = \fopen('/dev/urandom', 'rb')) !== false) 218 | { 219 | \stream_set_chunk_size($fp, $length); 220 | $output = \fread($fp, $length); 221 | \fclose($fp); 222 | if ($output !== false) 223 | { 224 | return ($rawOutput) ? $output : \bin2hex($output); 225 | } 226 | } 227 | 228 | throw new \RuntimeException('No reliable PRNG source is available on the system.'); 229 | // @codeCoverageIgnoreEnd 230 | } 231 | 232 | /** 233 | * hkdf() 234 | * 235 | * An RFC5869-compliant HMAC Key Derivation Function implementation. 236 | * 237 | * @link https://tools.ietf.org/rfc/rfc5869.txt 238 | * @param string $key Input key material 239 | * @param string $digest Hashing algorithm 240 | * @param int $length Desired output length 241 | * @param string $info Context/application-specific info 242 | * @param string $salt Salt 243 | * @return string A pseudo-random stream of bytes 244 | */ 245 | public static function hkdf($key, $digest, $length = null, $info = '', $salt = null) 246 | { 247 | static $digests; 248 | isset($digests) OR $digests = array('sha512' => 64); 249 | 250 | if ( ! isset($digests[$digest])) 251 | { 252 | if (\in_array($digest, \hash_algos(), true)) $digests[$digest] = self::strlen(\hash($digest, '', true)); 253 | else throw new \InvalidArgumentException('Unknown HKDF algorithm: '.$digest); 254 | } 255 | 256 | if ( ! isset($length)) 257 | { 258 | $length = $digests[$digest]; 259 | } 260 | elseif ( ! \is_int($length) OR $length < 1 OR $length > (255 * $digests[$digest])) 261 | { 262 | throw new \InvalidArgumentException('HKDF output length for '.$digest.' must be an integer between 1 and '.(255 * $digests[$digest])); 263 | } 264 | 265 | self::strlen($salt) OR $salt = \str_repeat("\x0", $digests[$digest]); 266 | $prk = \hash_hmac($digest, $key, $salt, true); 267 | $key = ''; 268 | for ($keyBlock = '', $blockIndex = 1; self::strlen($key) < $length; $blockIndex++) 269 | { 270 | $keyBlock = \hash_hmac($digest, $keyBlock.$info.\chr($blockIndex), $prk, true); 271 | $key .= $keyBlock; 272 | } 273 | 274 | return self::substr($key, 0, $length); 275 | } 276 | 277 | /** 278 | * authenticate() 279 | * 280 | * Validates and strips Base64 encoding, then separates the HMAC message from 281 | * the cipher text and verifies them in a way that prevents timing attacks. 282 | * 283 | * @param string &$cipherText Cipher text 284 | * @param string $hmacKey HMAC key 285 | * @return void 286 | */ 287 | private function authenticate(&$cipherText, $hmacKey) 288 | { 289 | if (($length = self::strlen($cipherText)) <= 32 OR ($length % 4) !== 0) 290 | { 291 | throw new \RuntimeException('Authentication failed: Invalid length'); 292 | } 293 | elseif (($cipherText = \base64_decode($cipherText, true)) === false) 294 | { 295 | // @codeCoverageIgnoreStart 296 | throw new \RuntimeException('Authentication failed: Input data is not a valid Base64 string.'); 297 | // @codeCoverageIgnoreEnd 298 | } 299 | 300 | $hmacRecv = self::substr($cipherText, 0, 32); 301 | $cipherText = self::substr($cipherText, 32); 302 | $hmacCalc = \hash_hmac('sha256', $cipherText, $hmacKey, true); 303 | 304 | /** 305 | * Double HMAC verification 306 | * 307 | * Protects against timing side-channel attacks by randomizing the 308 | * attacker's guess input instead of trying to directly compare in 309 | * a constant time fashion. The latter is apparently not always 310 | * possible due to run-time or compile-time optimizations. 311 | * 312 | * Reference: https://www.isecpartners.com/blog/2011/february/double-hmac-verification.aspx 313 | * 314 | * A note on MD5 usage here: 315 | * 316 | * As explained, the goal is simply to change the strings being 317 | * compared, so we don't need a strong algorithm, just a fast one. 318 | */ 319 | if (\hash_hmac('md5', $hmacRecv, $hmacKey) !== \hash_hmac('md5', $hmacCalc, $hmacKey)) 320 | { 321 | throw new \RuntimeException('Authentication failed: HMAC mismatch'); 322 | } 323 | } 324 | 325 | /** 326 | * __sleep() 327 | * 328 | * Prevents serialization to avoid accidental data leaks. 329 | */ 330 | public final function __sleep() 331 | { 332 | throw new \RuntimeException('Serialization is not allowed!'); 333 | return array(); 334 | } 335 | 336 | /** 337 | * strlen() 338 | * 339 | * We use this to make sure that we're counting bytes 340 | * instead of multibyte characters. 341 | * 342 | * @param string $string Input string 343 | * @return int 344 | */ 345 | private static function strlen($string) 346 | { 347 | return (\defined('MB_OVERLOAD_STRING')) 348 | ? \mb_strlen($string, '8bit') 349 | : \strlen($string); 350 | } 351 | 352 | /** 353 | * substr() 354 | * 355 | * We use this to make sure that we're cutting at byte 356 | * counts instead of multibyte character boundaries. 357 | * 358 | * @param string $string Input string 359 | * @param int $start Starting byte index 360 | * @param int $length Output string length 361 | * @return string Output string 362 | */ 363 | private static function substr($string, $start, $length = null) 364 | { 365 | if (\defined('MB_OVERLOAD_STRING')) 366 | { 367 | return \mb_substr($string, $start, $length, '8bit'); 368 | } 369 | 370 | // Unlike mb_substr(), substr() returns an empty string 371 | // if we pass null as the $length value. 372 | return isset($length) 373 | ? \substr($string, $start, $length) 374 | : \substr($string, $start); 375 | } 376 | 377 | } -------------------------------------------------------------------------------- /tests/SecretTest.php: -------------------------------------------------------------------------------- 1 | getMethod('strlen'); 15 | $substr = $reflection->getMethod('substr'); 16 | $strlen->setAccessible(true); 17 | $substr->setAccessible(true); 18 | 19 | $this->assertEquals(8, $strlen->invoke(null, 'осем'), 'Secret::strlen() is not byte-safe!'); 20 | 21 | $this->assertEquals('осем', $substr->invoke(null, 'осем', 0)); 22 | $this->assertEquals(7, $this->strlen($substr->invoke(null, 'осем', 1))); 23 | $this->assertEquals(2, $this->strlen($substr->invoke(null, 'осем', 1, 2))); 24 | $this->assertEquals(3, $this->strlen($substr->invoke(null, 'осем', 0, 3))); 25 | $this->assertEquals(1, $this->strlen($substr->invoke(null, 'осем', -1))); 26 | $this->assertEquals(1, $this->strlen($substr->invoke(null, 'осем', -3, 1))); 27 | // Throw-in a single-byte character, just in case 28 | $this->assertEquals('0с', $substr->invoke(null, '0сем', 0, 3), 'Secret::substr() is not byte-safe!'); 29 | } 30 | 31 | /** 32 | * strlen(), a byte-safe version 33 | * 34 | * @coversNothing 35 | */ 36 | private function strlen($str) 37 | { 38 | return defined('MB_OVERLOAD_STRING') ? mb_strlen($str, '8bit') : strlen($str); 39 | } 40 | 41 | /** 42 | * substr(), a byte-safe version 43 | * 44 | * @coversNothing 45 | */ 46 | private function substr($str, $start, $length = null) 47 | { 48 | if (defined('MB_OVERLOAD_STRING')) 49 | { 50 | return mb_substr($str, $start, $length, '8bit'); 51 | } 52 | 53 | return isset($length) 54 | ? substr($str, $start, $length) 55 | : substr($str, $start); 56 | } 57 | 58 | /** 59 | * __construct() input sanitization 60 | * 61 | * @depends testMbstringOverride 62 | */ 63 | public function testConstructInvalidParams() 64 | { 65 | // Invalid key, lower length 66 | $test = false; 67 | try { new Secret('dummy', str_repeat('0', rand(0,63))); } 68 | catch (InvalidArgumentException $e) { $test = true; } 69 | $this->assertTrue($test, 'Secret::__construct() accepts keys with invalid length.'); 70 | 71 | // Invalid key, higher length 72 | $test = false; 73 | try { new Secret('dummy', str_repeat('0', rand(65,128))); } 74 | catch (InvalidArgumentException $e) { $test = true; } 75 | $this->assertTrue($test, 'Secret::__construct() accepts keys with invalid length.'); 76 | 77 | // Invalid key, not hex 78 | $test = false; 79 | try { new Secret('dummy', str_repeat('0', rand(0, 63)).'g'); } 80 | catch (InvalidArgumentException $e) { $test = true; } 81 | $this->assertTrue($test, 'Secret::__construct() accepts non-hexadecimal keys.'); 82 | 83 | // Invalid input type 84 | $test = false; 85 | try { new Secret('dummy', str_repeat('0', 64), 'This triggers exception'); } 86 | catch (InvalidArgumentException $e) { $test = true; } 87 | $this->assertTrue($test, 'Secret::__construct() accepts invalid input types.'); 88 | 89 | // Type Secret::ENCRYPTED, but with no key (logical error) 90 | $test = false; 91 | try { new Secret('dummy', null, Secret::ENCRYPTED); } 92 | catch (InvalidArgumentException $e) { $test = true; } 93 | $this->assertTrue($test, 'Secret::__construct() accepts type Secret::ENCRYPTED with no key.'); 94 | } 95 | 96 | /** 97 | * __construct() valid usage tests 98 | * 99 | * @depends testConstructInvalidParams 100 | * @runInSeparateProcess 101 | */ 102 | public function testConstructValidUsage() 103 | { 104 | $instance = new Secret('A secret message'); 105 | $reflection = new ReflectionClass($instance); 106 | $inputText = $reflection->getProperty('inputText'); 107 | $inputType = $reflection->getProperty('inputType'); 108 | $masterKey = $reflection->getProperty('masterKey'); 109 | $inputText->setAccessible(true); 110 | $inputType->setAccessible(true); 111 | $masterKey->setAccessible(true); 112 | 113 | // Text only: $inputType = Secret::PLAINTEXT 114 | $this->assertEquals('A secret message', $inputText->getValue($instance), 'Secret::$inputText was not (properly) set.'); 115 | $this->assertEquals(Secret::PLAINTEXT, $inputType->getValue($instance), 'Secret::$inputType was not (properly) set.'); 116 | $this->assertNull($masterKey->getValue($instance), 'Secret::$masterKey is set, but it was not provided.'); 117 | 118 | // Text and key: $inputType = Secret::ENCRYPTED 119 | $instance = new Secret('Another secret message', str_repeat('01', 32)); 120 | $this->assertEquals('Another secret message', $inputText->getValue($instance), 'Secret::$inputText was not (properly) set.'); 121 | $this->assertEquals(Secret::ENCRYPTED, $inputType->getValue($instance), 'Secret::$inputType was not (properly) set.'); 122 | $this->assertEquals(str_repeat("\x1", 32), $masterKey->getValue($instance), 'Secret::$masterKey was not (properly) set.'); 123 | 124 | // Text, key and type (plaintext) 125 | $instance = new Secret('dummy', str_repeat('02', 32), Secret::PLAINTEXT); 126 | $this->assertEquals(Secret::PLAINTEXT, $inputType->getValue($instance), 'Secret::$inputType was not (properly) set.'); 127 | $this->assertEquals(str_repeat("\x2", 32), $masterKey->getValue($instance), 'Secret::$masterKey was not (properly) set.'); 128 | 129 | // Text, key and type (encrypted) 130 | $instance = new Secret('dummy', str_repeat('03', 32), Secret::ENCRYPTED); 131 | $this->assertEquals(Secret::ENCRYPTED, $inputType->getValue($instance), 'Secret::$inputType was not (properly) set.'); 132 | 133 | // Text and type, no key 134 | $instance = new Secret('dummy', null, Secret::PLAINTEXT); 135 | $this->assertEquals(Secret::PLAINTEXT, $inputType->getValue($instance), 'Secret::$inputType was not (properly) set.'); 136 | } 137 | 138 | /** 139 | * getRandomBytes() tests 140 | */ 141 | public function testGetRandomBytes() 142 | { 143 | $test = false; 144 | try { Secret::getRandomBytes('1'); } 145 | catch (InvalidArgumentException $e) { $test = true; } 146 | $this->assertTrue($test, 'Secret::getRandomBytes() accepts non-integer lenghts.'); 147 | 148 | $test = false; 149 | try { Secret::getRandomBytes(0); } 150 | catch (InvalidArgumentException $e) { $test = true; } 151 | $this->assertTrue($test, 'Secret::getRandomBytes() accepts zero lengths.'); 152 | 153 | $test = false; 154 | try { Secret::getRandomBytes(-1); } 155 | catch (InvalidArgumentException $e) { $test = true; } 156 | $this->assertTrue($test, 'Secret::getRandomBytes() accepts negative lengths.'); 157 | 158 | try 159 | { 160 | foreach (array(16, 32, 48) as $expectedLength) 161 | { 162 | // Default output type: hex-encoded 163 | $receivedLength = $this->strlen(Secret::getRandomBytes($expectedLength)); 164 | $this->assertEquals($expectedLength * 2, $receivedLength, 'Secret::getRandomBytes() returned '.$receivedLength.' characters, but '.($expectedLength * 2).' were expected.'); 165 | $receivedLength = $this->strlen(Secret::getRandomBytes($expectedLength, true)); 166 | $this->assertEquals($expectedLength, $receivedLength, 'Secret::getRandomBytes() returned '.$receivedLength.' bytes, but '.$expectedLength.' were expected.'); 167 | } 168 | } 169 | catch (RuntimeException $e) 170 | { 171 | $this->markTestIncomplete('No reliable PRNG is available.'); 172 | } 173 | } 174 | 175 | /** 176 | * getKey() with pre-set key tests 177 | * 178 | * @depends testConstructValidUsage 179 | */ 180 | public function testGetKeyWithKey() 181 | { 182 | $instance = new Secret('dummy', str_repeat('03', 32)); 183 | $this->assertEquals(str_repeat('03', 32), $instance->getKey(), 'Secret::getKey() returned a wrong key.'); 184 | $instance = new Secret('dummy', str_repeat('04', 32)); 185 | $this->assertEquals(str_repeat('04', 32), $instance->getKey(), 'Secret::getKey() returned a wrong key.'); 186 | } 187 | 188 | /** 189 | * getKey() with no pre-set key tests 190 | * 191 | * @depends testConstructValidUsage 192 | * @depends testGetRandomBytes 193 | */ 194 | public function testGetKeyNoKey() 195 | { 196 | $instance = new Secret('dummy'); 197 | $key = $instance->getKey(); 198 | $this->assertEquals(64, $this->strlen($key), 'Secret::getKey() returned a wrong key.'); 199 | // Make sure the generated key was retained 200 | $this->assertEquals($key, $instance->getKey(), 'Secret::getKey() does not retain self-generated keys.'); 201 | } 202 | 203 | /** 204 | * HMAC-SHA-2 tests 205 | * 206 | * Runs HMAC-SHA-2 test vectors, specified by RFC 4231. 207 | * http://www.ietf.org/rfc/rfc4231.txt 208 | * 209 | * @coversNothing 210 | */ 211 | public function testHMACSHA2() 212 | { 213 | // HMAC-SHA-2 tests 214 | // Test case 1 215 | $key = "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b"; 216 | $data = "Hi There"; 217 | 218 | $this->assertEquals( 219 | '896fb1128abbdf196832107cd49df33f47b4b1169912ba4f53684b22', 220 | hash_hmac('sha224', $data, $key, false), 221 | 'HMAC SHA-224 test vector 1 failed!' 222 | ); 223 | $this->assertEquals( 224 | 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7', 225 | hash_hmac('sha256', $data, $key, false), 226 | 'HMAC SHA-256 test vector 1 failed!' 227 | ); 228 | $this->assertEquals( 229 | 'afd03944d84895626b0825f4ab46907f15f9dadbe4101ec682aa034c7cebc59cfaea9ea9076ede7f4af152e8b2fa9cb6', 230 | hash_hmac('sha384', $data, $key, false), 231 | 'HMAC SHA-384 test vector 1 failed!' 232 | ); 233 | $this->assertEquals( 234 | '87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854', 235 | hash_hmac('sha512', $data, $key, false), 236 | 'HMAC SHA-512 test vector 1 failed!' 237 | ); 238 | 239 | // Test case 2: Test with a key shorter than the length of the HMAC output 240 | $key = "\x4a\x65\x66\x65"; 241 | $data = "what do ya want for nothing?"; 242 | 243 | $this->assertEquals( 244 | 'a30e01098bc6dbbf45690f3a7e9e6d0f8bbea2a39e6148008fd05e44', 245 | hash_hmac('sha224', $data, $key, false), 246 | 'HMAC SHA-224 test vector 2 failed!' 247 | ); 248 | $this->assertEquals( 249 | '5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843', 250 | hash_hmac('sha256', $data, $key, false), 251 | 'HMAC SHA-256 test vector 2 failed!' 252 | ); 253 | $this->assertEquals( 254 | 'af45d2e376484031617f78d2b58a6b1b9c7ef464f5a01b47e42ec3736322445e8e2240ca5e69e2c78b3239ecfab21649', 255 | hash_hmac('sha384', $data, $key, false), 256 | 'HMAC SHA-384 test vector 2 failed!' 257 | ); 258 | $this->assertEquals( 259 | '164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737', 260 | hash_hmac('sha512', $data, $key, false), 261 | 'HMAC SHA-512 test vector 2 failed!' 262 | ); 263 | 264 | // Test case 3: Test with a combined length of key and data that is larger than 64 bytes (=block-size of SHA-224 and SHA-256) 265 | $key = "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"; 266 | $data = "\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd"; 267 | 268 | $this->assertEquals( 269 | '7fb3cb3588c6c1f6ffa9694d7d6ad2649365b0c1f65d69d1ec8333ea', 270 | hash_hmac('sha224', $data, $key, false), 271 | 'HMAC SHA-224 test vector 3 failed!' 272 | ); 273 | $this->assertEquals( 274 | '773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe', 275 | hash_hmac('sha256', $data, $key, false), 276 | 'HMAC SHA-256 test vector 3 failed!' 277 | ); 278 | $this->assertEquals( 279 | '88062608d3e6ad8a0aa2ace014c8a86f0aa635d947ac9febe83ef4e55966144b2a5ab39dc13814b94e3ab6e101a34f27', 280 | hash_hmac('sha384', $data, $key, false), 281 | 'HMAC SHA-384 test vector 3 failed!' 282 | ); 283 | $this->assertEquals( 284 | 'fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb', 285 | hash_hmac('sha512', $data, $key, false), 286 | 'HMAC SHA-512 test vector 3 failed!' 287 | ); 288 | 289 | // Test case 4: Test with combined length of key and data that is larger than 64 bytes (= block-size of SHA-224 and SHA-256) 290 | $key = "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19"; 291 | $data = "\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd"; 292 | 293 | $this->assertEquals( 294 | '6c11506874013cac6a2abc1bb382627cec6a90d86efc012de7afec5a', 295 | hash_hmac('sha224', $data, $key, false), 296 | 'HMAC SHA-224 test vector 4 failed!' 297 | ); 298 | $this->assertEquals( 299 | '82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b', 300 | hash_hmac('sha256', $data, $key, false), 301 | 'HMAC SHA-256 test vector 4 failed!' 302 | ); 303 | $this->assertEquals( 304 | '3e8a69b7783c25851933ab6290af6ca77a9981480850009cc5577c6e1f573b4e6801dd23c4a7d679ccf8a386c674cffb', 305 | hash_hmac('sha384', $data, $key, false), 306 | 'HMAC SHA-384 test vector 4 failed!' 307 | ); 308 | $this->assertEquals( 309 | 'b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3dba91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd', 310 | hash_hmac('sha512', $data, $key, false), 311 | 'HMAC SHA-512 test vector 4 failed!' 312 | ); 313 | 314 | // Test case 5: Test with a truncation of output to 128 bits 315 | $key = "\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c"; 316 | $data = "Test With Truncation"; 317 | 318 | $this->assertEquals( 319 | '0e2aea68a90c8d37c988bcdb9fca6fa8', 320 | substr(hash_hmac('sha224', $data, $key, false), 0, 32), 321 | 'HMAC SHA-224 test vector 5 failed!' 322 | ); 323 | $this->assertEquals( 324 | 'a3b6167473100ee06e0c796c2955552b', 325 | substr(hash_hmac('sha256', $data, $key, false), 0, 32), 326 | 'HMAC SHA-256 test vector 5 failed!' 327 | ); 328 | $this->assertEquals( 329 | '3abf34c3503b2a23a46efc619baef897', 330 | substr(hash_hmac('sha384', $data, $key, false), 0, 32), 331 | 'HMAC SHA-384 test vector 5 failed!' 332 | ); 333 | $this->assertEquals( 334 | '415fad6271580a531d4179bc891d87a6', 335 | substr(hash_hmac('sha512', $data, $key, false), 0, 32), 336 | 'HMAC SHA-512 test vector 5 failed!' 337 | ); 338 | 339 | // Test case 6: Test with a key larger than 128 bytes (= block-size of SHA-384 and SHA-512) 340 | $key = "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"; 341 | $data = "Test Using Larger Than Block-Size Key - Hash Key First"; 342 | 343 | $this->assertEquals( 344 | '95e9a0db962095adaebe9b2d6f0dbce2d499f112f2d2b7273fa6870e', 345 | hash_hmac('sha224', $data, $key, false), 346 | 'HMAC SHA-224 test vector 6 failed!' 347 | ); 348 | $this->assertEquals( 349 | '60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54', 350 | hash_hmac('sha256', $data, $key, false), 351 | 'HMAC SHA-256 test vector 6 failed!' 352 | ); 353 | $this->assertEquals( 354 | '4ece084485813e9088d2c63a041bc5b44f9ef1012a2b588f3cd11f05033ac4c60c2ef6ab4030fe8296248df163f44952', 355 | hash_hmac('sha384', $data, $key, false), 356 | 'HMAC SHA-384 test vector 6 failed!' 357 | ); 358 | $this->assertEquals( 359 | '80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f3526b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598', 360 | hash_hmac('sha512', $data, $key, false), 361 | 'HMAC SHA-512 test vector 6 failed!' 362 | ); 363 | 364 | // Test case 7: Test with a key and data that is larger than 128 bytes (= block-size of SHA-384 and SHA-512) 365 | $key = "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"; 366 | $data = "This is a test using a larger than block-size key and a larger than block-size data. The key needs to be hashed before being used by the HMAC algorithm."; 367 | 368 | $this->assertEquals( 369 | '3a854166ac5d9f023f54d517d0b39dbd946770db9c2b95c9f6f565d1', 370 | hash_hmac('sha224', $data, $key, false), 371 | 'HMAC SHA-224 test vector 7 failed!' 372 | ); 373 | $this->assertEquals( 374 | '9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2', 375 | hash_hmac('sha256', $data, $key, false), 376 | 'HMAC SHA-256 test vector 7 failed!' 377 | ); 378 | $this->assertEquals( 379 | '6617178e941f020d351e2f254e8fd32c602420feb0b8fb9adccebb82461e99c5a678cc31e799176d3860e6110c46523e', 380 | hash_hmac('sha384', $data, $key, false), 381 | 'HMAC SHA-384 test vector 7 failed!' 382 | ); 383 | $this->assertEquals( 384 | 'e37b6a775dc87dbaa4dfa9f96e5e3ffddebd71f8867289865df5a32d20cdc944b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58', 385 | hash_hmac('sha512', $data, $key, false), 386 | 'HMAC SHA-512 test vector 7 failed!' 387 | ); 388 | } 389 | 390 | /** 391 | * hkdf() tests 392 | * 393 | * Runs test vectors specified by RFC 5689, Appendix A. 394 | * https://tools.ietf.org/rfc/rfc5869.txt 395 | * 396 | * Because our implementation is a single method instead of being 397 | * split into hkdf_extract() and hkdf_expand(), we cannot test for 398 | * the PRK value. As long as the OKM is correct though, it's fine. 399 | * 400 | * @depends testHMACSHA2 401 | */ 402 | public function testHKDF() 403 | { 404 | // A.1: Basic test case with SHA-256 405 | $this->assertEquals( 406 | "\x3c\xb2\x5f\x25\xfa\xac\xd5\x7a\x90\x43\x4f\x64\xd0\x36\x2f\x2a\x2d\x2d\x0a\x90\xcf\x1a\x5a\x4c\x5d\xb0\x2d\x56\xec\xc4\xc5\xbf\x34\x00\x72\x08\xd5\xb8\x87\x18\x58\x65", 407 | Secret::hkdf( 408 | "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", 409 | 'sha256', 410 | 42, 411 | "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9", 412 | "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c" 413 | ), 414 | 'HKDF test vector 1 failed!' 415 | ); 416 | // A.2: Test with SHA-256 and longer inputs/outputs 417 | $this->assertEquals( 418 | "\xb1\x1e\x39\x8d\xc8\x03\x27\xa1\xc8\xe7\xf7\x8c\x59\x6a\x49\x34\x4f\x01\x2e\xda\x2d\x4e\xfa\xd8\xa0\x50\xcc\x4c\x19\xaf\xa9\x7c\x59\x04\x5a\x99\xca\xc7\x82\x72\x71\xcb\x41\xc6\x5e\x59\x0e\x09\xda\x32\x75\x60\x0c\x2f\x09\xb8\x36\x77\x93\xa9\xac\xa3\xdb\x71\xcc\x30\xc5\x81\x79\xec\x3e\x87\xc1\x4c\x01\xd5\xc1\xf3\x43\x4f\x1d\x87", 419 | Secret::hkdf( 420 | "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f", 421 | 'sha256', 422 | 82, 423 | "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff", 424 | "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf" 425 | ), 426 | 'HKDF test vector 2 failed!' 427 | ); 428 | // A.3: Test with SHA-256 and zero-length salt/info 429 | $this->assertEquals( 430 | "\x8d\xa4\xe7\x75\xa5\x63\xc1\x8f\x71\x5f\x80\x2a\x06\x3c\x5a\x31\xb8\xa1\x1f\x5c\x5e\xe1\x87\x9e\xc3\x45\x4e\x5f\x3c\x73\x8d\x2d\x9d\x20\x13\x95\xfa\xa4\xb6\x1a\x96\xc8", 431 | Secret::hkdf( 432 | "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", 433 | 'sha256', 434 | 42, 435 | '', 436 | null 437 | ), 438 | 'HKDF test vector 3 failed!' 439 | ); 440 | // A.4: Basic test case with SHA-1 441 | $this->assertEquals( 442 | "\x08\x5a\x01\xea\x1b\x10\xf3\x69\x33\x06\x8b\x56\xef\xa5\xad\x81\xa4\xf1\x4b\x82\x2f\x5b\x09\x15\x68\xa9\xcd\xd4\xf1\x55\xfd\xa2\xc2\x2e\x42\x24\x78\xd3\x05\xf3\xf8\x96", 443 | Secret::hkdf( 444 | "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", 445 | 'sha1', 446 | 42, 447 | "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9", 448 | "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c" 449 | ), 450 | 'HKDF test vector 4 failed!' 451 | ); 452 | // A.5: Test with SHA-1 and longer inputs/output 453 | $this->assertEquals( 454 | "\x0b\xd7\x70\xa7\x4d\x11\x60\xf7\xc9\xf1\x2c\xd5\x91\x2a\x06\xeb\xff\x6a\xdc\xae\x89\x9d\x92\x19\x1f\xe4\x30\x56\x73\xba\x2f\xfe\x8f\xa3\xf1\xa4\xe5\xad\x79\xf3\xf3\x34\xb3\xb2\x02\xb2\x17\x3c\x48\x6e\xa3\x7c\xe3\xd3\x97\xed\x03\x4c\x7f\x9d\xfe\xb1\x5c\x5e\x92\x73\x36\xd0\x44\x1f\x4c\x43\x00\xe2\xcf\xf0\xd0\x90\x0b\x52\xd3\xb4", 455 | Secret::hkdf( 456 | "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f", 457 | 'sha1', 458 | 82, 459 | "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff", 460 | "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf" 461 | ), 462 | 'HKDF test vector 5 failed!' 463 | ); 464 | // A.6: Test with SHA-1 and zero-length salt/info 465 | $this->assertEquals( 466 | "\x0a\xc1\xaf\x70\x02\xb3\xd7\x61\xd1\xe5\x52\x98\xda\x9d\x05\x06\xb9\xae\x52\x05\x72\x20\xa3\x06\xe0\x7b\x6b\x87\xe8\xdf\x21\xd0\xea\x00\x03\x3d\xe0\x39\x84\xd3\x49\x18", 467 | Secret::hkdf( 468 | "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", 469 | 'sha1', 470 | 42, 471 | '', 472 | null 473 | ), 474 | 'HKDF test vector 6 failed!' 475 | ); 476 | // A.7: Test with SHA-1, salt not provided (defaults to HashLen zero octets), zero-length info 477 | $this->assertEquals( 478 | "\x2c\x91\x11\x72\x04\xd7\x45\xf3\x50\x0d\x63\x6a\x62\xf6\x4f\x0a\xb3\xba\xe5\x48\xaa\x53\xd4\x23\xb0\xd1\xf2\x7e\xbb\xa6\xf5\xe5\x67\x3a\x08\x1d\x70\xcc\xe7\xac\xfc\x48", 479 | Secret::hkdf( 480 | "\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c", 481 | 'sha1', 482 | 42, 483 | '' 484 | ), 485 | 'HKDF test vector 7 failed!' 486 | ); 487 | 488 | // Test default length, it must match the digest size 489 | $this->assertEquals(64, $this->strlen(Secret::hkdf('foobar', 'sha512')), 'Secret::hkdf() default output length does not match the size of the hash function output.'); 490 | 491 | // Test maximum length (RFC5869 says that it must be up to 255 times the digest size) 492 | $this->assertEquals(8160, $this->strlen(Secret::hkdf('foobar', 'sha256', 32 * 255)), 'Secret::hkdf() cannot return OKM with a length of 255 times the hash function output.'); 493 | 494 | // Invalid length 495 | $test = false; 496 | try { Secret::hkdf('foobar', 'whirlpool', 64 * 255 + 1); } 497 | catch (InvalidArgumentException $e) { $test = true; } 498 | $this->assertTrue($test, 'Secret::hkdf() accepts lengths larger than 255 times the hash function output.'); 499 | 500 | // Invalid hash function 501 | $test = false; 502 | try { Secret::hkdf('foobar', ''); } 503 | catch (InvalidArgumentException $e) { $test = true; } 504 | $this->assertTrue($test, 'Secret::hkdf() accepts unknown hash functions.'); 505 | } 506 | 507 | /** 508 | * authenticate() test 509 | * 510 | * @depends testHMACSHA2 511 | */ 512 | public function testAuthenticate() 513 | { 514 | $instance = new Secret('plain-text'); 515 | $reflection = new ReflectionClass($instance); 516 | $authenticate = $reflection->getMethod('authenticate'); 517 | $authenticate->setAccessible(true); 518 | 519 | // Note: authenticate() accepts the cipherText by reference and 520 | // ReflectionMethod::invoke() is dumb and doesn't understand 521 | // references, so we have to use invokeArgs() instead ... 522 | 523 | // Invalid length, shorter than the hash size 524 | $test = false; 525 | $variable = 'shorter than 32 characters'; 526 | try { $authenticate->invokeArgs($instance, array(&$variable, 'hmacKey')); } 527 | catch (RuntimeException $e) { $test = true; } 528 | $this->assertTrue($test, 'Secret::authenticate() accepts messages with invalid lengths.'); 529 | 530 | // Invalid length, longer than the hash size, but not dividable by 4 (this is a Base64-validity check too) 531 | $test = false; 532 | $variable = str_repeat('0', 33); 533 | try { $authenticate->invokeArgs($instance, array(&$variable, 'hmacKey')); } 534 | catch (RuntimeException $e) { $test = true; } 535 | $this->assertTrue($test, 'Secret::authenticate() accepts messages with invalid lengths.'); 536 | 537 | // Valid length, but not valid Base64 538 | $test = false; 539 | $variable = str_repeat('1', 31).'$'; 540 | try { $authenticate->invokeArgs($instance, array(&$variable, 'hmacKey')); } 541 | catch (RuntimeException $e) { $test = true; } 542 | $this->assertTrue($test, 'Secret::authenticate() accepts invalid Base64 strings.'); 543 | 544 | // Invalid key 545 | $test = false; 546 | $variable = "\xb0\x34\x4c\x61\xd8\xdb\x38\x53\x5c\xa8\xaf\xce\xaf\x0b\xf1\x2b\x88\x1d\xc2\x00\xc9\x83\x3d\xa7\x26\xe9\x37\x6c\x2e\x32\xcf\xf7"; 547 | try 548 | { 549 | $authenticate->invokeArgs( 550 | $instance, 551 | array(&$variable, "\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a") 552 | ); 553 | } 554 | catch (RuntimeException $e) { $test = true; } 555 | $this->assertTrue($test, 'Secret::authenticate() failed to trigger an error for a HMAC with a wrong key.'); 556 | 557 | // Invalid hash 558 | $test = false; 559 | $variable = "\xa0\x34\x4c\x61\xd8\xdb\x38\x53\x5c\xa8\xaf\xce\xaf\x0b\xf1\x2b\x88\x1d\xc2\x00\xc9\x83\x3d\xa7\x26\xe9\x37\x6c\x2e\x32\xcf\xf7"; 560 | try 561 | { 562 | $authenticate->invokeArgs( 563 | $instance, 564 | array(&$variable, "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b") 565 | ); 566 | } 567 | catch (RuntimeException $e) { $test = true; } 568 | $this->assertTrue($test, 'Secret::authenticate() failed to trigger an error for a forged HMAC.'); 569 | 570 | // Valid usage, should strip the Base64 encoding and HMAC after validating it 571 | $data = 'dummy string'; 572 | $data = base64_encode(hash_hmac('sha256', $data, str_repeat('32', 32), true).$data); 573 | $authenticate->invokeArgs($instance, array(&$data, str_repeat('32', 32))); 574 | $this->assertEquals($data, 'dummy string', 'Secret::authenticate() does not strip Base64 encoding and/or the HMAC message after validating them.'); 575 | } 576 | 577 | /** 578 | * AES-256-CTR tests 579 | * 580 | * Runs AES-256-CTR test vectors, as specified by NIST SP 800-38A, Appendix F.5. 581 | * http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf 582 | * 583 | * @coversNothing 584 | * @runInSeparateProcess 585 | */ 586 | public function testAES256CTR() 587 | { 588 | // AES-256-CTR tests 589 | // All data matches for the encrypt, decrypt tests 590 | $vectorsKey = "\x60\x3d\xeb\x10\x15\xca\x71\xbe\x2b\x73\xae\xf0\x85\x7d\x77\x81\x1f\x35\x2c\x07\x3b\x61\x08\xd7\x2d\x98\x10\xa3\x09\x14\xdf\xf4"; 591 | $vectors = array( 592 | // Block #1 593 | 1 => array( 594 | 'iv' => "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff", 595 | 'plainText' => "\x6b\xc1\xbe\xe2\x2e\x40\x9f\x96\xe9\x3d\x7e\x11\x73\x93\x17\x2a", 596 | 'cipherText' => "\x60\x1e\xc3\x13\x77\x57\x89\xa5\xb7\xa7\xf5\x04\xbb\xf3\xd2\x28" 597 | ), 598 | // Block #2 599 | 2 => array( 600 | 'iv' => "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xff\x00", 601 | 'plainText' => "\xae\x2d\x8a\x57\x1e\x03\xac\x9c\x9e\xb7\x6f\xac\x45\xaf\x8e\x51", 602 | 'cipherText' => "\xf4\x43\xe3\xca\x4d\x62\xb5\x9a\xca\x84\xe9\x90\xca\xca\xf5\xc5" 603 | ), 604 | // Block #3 605 | 3 => array( 606 | 'iv' => "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xff\x01", 607 | 'plainText' => "\x30\xc8\x1c\x46\xa3\x5c\xe4\x11\xe5\xfb\xc1\x19\x1a\x0a\x52\xef", 608 | 'cipherText' => "\x2b\x09\x30\xda\xa2\x3d\xe9\x4c\xe8\x70\x17\xba\x2d\x84\x98\x8d" 609 | ), 610 | // Block #4 611 | 4 => array( 612 | 'iv' => "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xff\x02", 613 | 'plainText' => "\xf6\x9f\x24\x45\xdf\x4f\x9b\x17\xad\x2b\x41\x7b\xe6\x6c\x37\x10", 614 | 'cipherText' => "\xdf\xc9\xc5\x8d\xb6\x7a\xad\xa6\x13\xc2\xdd\x08\x45\x79\x41\xa6" 615 | ), 616 | ); 617 | 618 | foreach ($vectors as $block => $test) 619 | { 620 | $this->assertEquals( 621 | $test['cipherText'], 622 | openssl_encrypt($test['plainText'], 'aes-256-ctr', $vectorsKey, 1, $test['iv']), 623 | 'AES-256-CTR test vector '.$block.' failed!' 624 | ); 625 | $this->assertEquals( 626 | $test['plainText'], 627 | openssl_decrypt($test['cipherText'], 'aes-256-ctr', $vectorsKey, 1, $test['iv']), 628 | 'AES-256-CTR test vector '.$block.' failed!' 629 | ); 630 | } 631 | } 632 | 633 | /** 634 | * getPlainText(), getCipherText(), overall usage tests 635 | * 636 | * @depends testConstructValidUsage 637 | * @depends testGetKeyWithKey 638 | * @depends testGetKeyNoKey 639 | * @depends testAuthenticate 640 | * @depends testHKDF 641 | * @depends testAES256CTR 642 | * @runInSeparateProcess 643 | */ 644 | public function testUsage() 645 | { 646 | try 647 | { 648 | // Test encryption 649 | $instance = new Secret('Test message'); 650 | $cipherText = $instance->getCipherText(); 651 | $this->assertEquals(1, preg_match('#^[A-Za-z0-9+=/]{80}$#', $cipherText), 'Secret::getCipherText() produced an unexpected result.'); 652 | // A 128-bit key should be automatically generated 653 | $reflection = new ReflectionClass($instance); 654 | $key = $reflection->getProperty('masterKey'); 655 | $key->setAccessible(true); 656 | $this->assertEquals(32, $this->strlen($key->getValue($instance)), 'Secret::getCipherText() does not (properly) generate keys.'); 657 | 658 | // A new getCipherText() call shouldn't produce the same output 659 | $this->assertNotEquals($cipherText, $instance->getCipherText(), 'Secret::getCipherText() produced the same cipherText in a subsequent call.'); 660 | 661 | // Now decrypt with the key we've got 662 | $instance = new Secret($cipherText, bin2hex($key->getValue($instance))); 663 | $this->assertEquals('Test message', $instance->getPlainText(), 'Secret::getPlainText() does not properly decrypt data.'); 664 | 665 | // Again, any getCipherText() call should encrypt anew, with a new IV 666 | $cipherTextNew = $instance->getCipherText(); 667 | $this->assertNotEquals($cipherText, $cipherTextNew, 'Secret::getCipherText() produced the same cipherText that was previously decrypted.'); 668 | // We'll check the IVs as well 669 | $this->assertNotEquals( 670 | $this->substr(base64_decode($cipherText, true), 32, 16), 671 | $this->substr(base64_decode($cipherTextNew, true), 32, 16), 672 | 'Secret::getCipherText() reuses IVs!' 673 | ); 674 | 675 | // getCipherText() shouldn't generate keys if we have provided them 676 | $instance = new Secret('Another test', str_repeat('0', 64), Secret::PLAINTEXT); 677 | $instance->getCipherText(); // If the next assertion fails, this is the problem 678 | $this->assertEquals(str_repeat('0', 64), $instance->getKey(), 'Secret::getCipherText() generates keys even when they were provided.'); 679 | 680 | // Our plain-text should be returned too 681 | $this->assertEquals('Another test', $instance->getPlainText()); 682 | } 683 | catch (RuntimeException $e) 684 | { 685 | $this->markTestIncomplete('PRNG error'); 686 | } 687 | } 688 | 689 | /** 690 | * Key derivation test 691 | * 692 | * @depends testUsage 693 | * @depends testHKDF 694 | */ 695 | public function testKeyDerivation() 696 | { 697 | $instance = new Secret('Test', str_repeat('af', 32), Secret::PLAINTEXT); 698 | $cipherText = $instance->getCipherText(); 699 | list(, $hmacKey) = str_split(Secret::hkdf(str_repeat("\xaf", 32), 'sha512', 64, 'aes-256-ctr-hmac-sha256'), 32); 700 | $this->assertEquals( 701 | hash_hmac('sha256', $this->substr(base64_decode($cipherText), 32), $hmacKey, true), 702 | $this->substr(base64_decode($cipherText), 0, 32), 703 | 'Secret::getCipherText() did not properly derive keys.' 704 | ); 705 | } 706 | 707 | /** 708 | * Serialization protection test 709 | * 710 | * @depends testConstructValidUsage 711 | * @expectedException RuntimeException 712 | */ 713 | public function testSerialization() 714 | { 715 | $instance = new Secret('Serialize this'); 716 | serialize($instance); 717 | } 718 | 719 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | ./ 13 | 14 | 15 | 16 | 17 | ../src 18 | 19 | ../vendor 20 | 21 | 22 | 23 | --------------------------------------------------------------------------------