├── ChangeLog.md
├── .travis.yml
├── tests
├── bootstrap.php
├── phpunit.xml
└── SecretTest.php
├── composer.json
├── README.md
└── src
└── Secret.php
/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.
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 | ./
13 |
14 |
15 |
16 |
17 | ../src
18 |
19 | ../vendor
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | [](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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------