PageRenderTime 48ms CodeModel.GetById 30ms app.highlight 13ms RepoModel.GetById 2ms app.codeStats 0ms

/storage/session/strategy/Encrypt.php

http://github.com/UnionOfRAD/lithium
PHP | 432 lines | 240 code | 33 blank | 159 comment | 20 complexity | b34ca076216d2ea47dc057e86ed435e0 MD5 | raw file
  1<?php
  2/**
  3 * li₃: the most RAD framework for PHP (http://li3.me)
  4 *
  5 * Copyright 2011, Union of RAD. All rights reserved. This source
  6 * code is distributed under the terms of the BSD 3-Clause License.
  7 * The full license text can be found in the LICENSE.txt file.
  8 */
  9
 10namespace lithium\storage\session\strategy;
 11
 12use lithium\core\ConfigException;
 13use lithium\security\Random;
 14
 15/**
 16 * This strategy allows you to encrypt your `Session` and / or `Cookie` data so that it
 17 * is not stored in cleartext on the client side. You must provide a secret key, otherwise
 18 * an exception is raised.
 19 *
 20 * To use this class, you need to have the `openssl` extension enabled.
 21 *
 22 * Example configuration:
 23 *
 24 * ```
 25 * Session::config(['default' => [
 26 *    'adapter' => 'Cookie',
 27 *    'strategies' => ['Encrypt' => ['secret' => 'f00bar$l1thium']]
 28 * ]]);
 29 * ```
 30 *
 31 * By default, this strategy uses the AES algorithm in the CBC mode. This means that an
 32 * initialization vector has to be generated and transported with the payload data. This
 33 * is done transparently, but you may want to keep this in mind (the ECB mode doesn't require
 34 * an initialization vector but is not recommended to use as it's insecure).
 35*
 36 * Please keep in mind that it is generally not a good idea to store sensitive information in
 37 * cookies (or generally on the client side) and this class is no exception to the rule. It allows
 38 * you to store client side data in a more secure way, but 100% security can't be achieved.
 39 *
 40 * Also note that if you provide a secret that is shorter than the maximum key length of the
 41 * algorithm used, the secret will be hashed to make it more secure. This also means that if you
 42 * want to use your own hashing algorithm, make sure it has the maximum key length of the algorithm
 43 * used. See the `Encrypt::_hashSecret()` method for more information on this.
 44 *
 45 * ## Legacy Mode
 46 *
 47 * This class previously used the now deprecated `mcrypt` extension and has been migrated
 48 * to use of the `openssl` extension for better support and performance. For backwards
 49 * compatibility reasons this class supports a _legacy_ mode in which `mcrypt` will be
 50 * used. The class will switch to legacy mode whenever it is not possible to use openssl
 51 * as a drop in.
 52 *
 53 * First, legacy mode will be triggered when the `openssl` extension is not available.
 54 *
 55 * Second, previously overriding the default cipher and mode were possible (see example
 56 * below). As we only support AES-256 in CBC mode (equals `mcrypt`'s RIJNDAEL_128 with
 57 * MODE_CBC) with the `openssl` extension, overrding the defaults will trigger legacy
 58 * mode.
 59 *
 60 * ```
 61 * Session::config(['default' => [
 62 *     'adapter' => 'Cookie',
 63 *     'strategies' => ['Encrypt' => [
 64 *         'cipher' => MCRYPT_RIJNDAEL_256,
 65 *         'mode' => MCRYPT_MODE_ECB, // Don't use ECB when you don't have to!
 66 *         'secret' => 'f00bar$l1thium'
 67 *     ]]
 68 * ]]);
 69 * ```
 70 *
 71 * @link http://php.net/book.openssl.php
 72 * @link http://php.net/book.mcrypt.php The mcrypt extension.
 73 * @link http://php.net/mcrypt.ciphers.php List of supported ciphers.
 74 * @link http://php.net/mcrypt.constants.php List of supported modes.
 75 */
 76class Encrypt extends \lithium\core\ObjectDeprecated {
 77
 78	/**
 79	 * Default configuration.
 80	 */
 81	protected $_defaults = [
 82		'secret' => null
 83	];
 84
 85	/**
 86	 * Constructor.
 87	 *
 88	 * @param array $config Configuration array.
 89	 * @return void
 90	 */
 91	public function __construct(array $config = []) {
 92		if (!isset($config['secret'])) {
 93			throw new ConfigException('Encrypt strategy requires a secret key.');
 94		}
 95
 96		if ($this->_mcrypt = $this->_mcrypt($config)) {
 97			if (!extension_loaded('mcrypt')) {
 98				throw new ConfigException('The mcrypt extension is not installed or enabled.');
 99			}
100			parent::__construct($config + $this->_defaults + [
101				'cipher' => MCRYPT_RIJNDAEL_128,
102				'mode' => MCRYPT_MODE_CBC
103			]);
104			$this->_mcryptResource = mcrypt_module_open(
105				$this->_config['cipher'], '', $this->_config['mode'], ''
106			);
107		} else {
108			if (!extension_loaded('openssl')) {
109				throw new ConfigException('The `openssl` extension is not installed or enabled.');
110			}
111			parent::__construct($config + $this->_defaults);
112		}
113	}
114
115	/**
116	 * Read encryption method.
117	 *
118	 * @param array $data the Data being read.
119	 * @param array $options Options for this method.
120	 * @return mixed Returns the decrypted data after it was read.
121	 */
122	public function read($data, array $options = []) {
123		$class = $options['class'];
124
125		$encrypted = $class::read(null, ['strategies' => false]);
126		$key = isset($options['key']) ? $options['key'] : null;
127
128		if (!isset($encrypted['__encrypted']) || !$encrypted['__encrypted']) {
129			return isset($encrypted[$key]) ? $encrypted[$key] : null;
130		}
131
132		if ($this->_mcrypt) {
133			$current = $this->_bcDecrypt($encrypted['__encrypted']);
134		} else {
135			$current = $this->_decrypt($encrypted['__encrypted']);
136		}
137
138		if ($key) {
139			return isset($current[$key]) ? $current[$key] : null;
140		}
141		return $current;
142	}
143
144	/**
145	 * Write encryption method.
146	 *
147	 * @param mixed $data The data to be encrypted.
148	 * @param array $options Options for this method.
149	 * @return string Returns the encrypted data that was written.
150	 */
151	public function write($data, array $options = []) {
152		$class = $options['class'];
153
154		$futureData = $this->read(null, ['key' => null] + $options) ?: [];
155		$futureData = [$options['key'] => $data] + $futureData;
156
157		$payload = null;
158
159		if (!empty($futureData)) {
160			if ($this->_mcrypt) {
161				$payload = $this->_bcEncrypt($futureData);
162			} else {
163				$payload = $this->_encrypt($futureData);
164			}
165		}
166
167		$class::write('__encrypted', $payload, ['strategies' => false] + $options);
168		return $payload;
169	}
170
171	/**
172	 * Delete encryption method.
173	 *
174	 * @param mixed $data The data to be encrypted.
175	 * @param array $options Options for this method.
176	 * @return string Returns the deleted data in cleartext.
177	 */
178	public function delete($data, array $options = []) {
179		$class = $options['class'];
180
181		$futureData = $this->read(null, ['key' => null] + $options) ?: [];
182		unset($futureData[$options['key']]);
183
184		$payload = null;
185
186		if (!empty($futureData)) {
187			if ($this->_mcrypt) {
188				$payload = $this->_bcEncrypt($futureData);
189			} else {
190				$payload = $this->_encrypt($futureData);
191			}
192		}
193
194		$class::write('__encrypted', $payload, ['strategies' => false] + $options);
195		return $data;
196	}
197
198	/**
199	 * Serialize and encrypt a given data array.
200	 *
201	 * @param array $decrypted The cleartext data to be encrypted.
202	 * @return string A Base64 encoded and encrypted string.
203	 */
204	protected function _encrypt($decrypted = []) {
205		$encrypted = openssl_encrypt(
206			serialize($decrypted),
207			'aes-256-cbc',
208			$this->_hashSecret($this->_config['secret']),
209			OPENSSL_RAW_DATA,
210			$vector = $this->_vector()
211		);
212		return base64_encode($encrypted) . base64_encode($vector);
213	}
214
215	/**
216	 * Decrypt and unserialize a previously encrypted string.
217	 *
218	 * @param string $encrypted The base64 encoded and encrypted string.
219	 * @return array The cleartext data.
220	 */
221	protected function _decrypt($encrypted) {
222		$secret = $this->_hashSecret($this->_config['secret']);
223
224		$vectorSize = strlen(base64_encode(str_repeat(' ', $this->_vectorSize())));
225		$vector = base64_decode(substr($encrypted, -$vectorSize));
226		$data = base64_decode(substr($encrypted, 0, -$vectorSize));
227
228		$decrypted = openssl_decrypt(
229			$data,
230			'aes-256-cbc',
231			$secret,
232			OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING,
233			$vector
234		);
235		return unserialize(trim($decrypted));
236	}
237
238	/**
239	 * Determines if the `mcrypt` or `openssl` extension has been installed.
240	 *
241	 * @return boolean `true` if enabled, `false` otherwise.
242	 */
243	public static function enabled() {
244		return extension_loaded('openssl') || extension_loaded('mcrypt');
245	}
246
247	/**
248	 * Hashes the given secret to make harder to detect.
249	 *
250	 * This method figures out the appropriate key size for the chosen encryption algorithm and
251	 * then hashes the given key accordingly. Note that if the key has already the needed length,
252	 * it is considered to be hashed (secure) already and is therefore not hashed again. This lets
253	 * you change the hashing method in your own code if you like.
254	 *
255	 * The default `aes-256-cbc` key should be 32 byte long `sha256` is used as the
256	 * hashing algorithm. If the key size is shorter than the one generated by `sha256`,
257	 * the first n bytes will be used.
258	 *
259	 * @param string $key The possibly too weak key.
260	 * @return string The hashed (raw) key.
261	 */
262	protected function _hashSecret($key) {
263		if (strlen($key) >= 32) {
264			return $key;
265		}
266		return substr(hash('sha256', $key, true), 0, 32);
267	}
268
269	/**
270	 * Generates an initialization vector.
271	 *
272	 * @return string Returns an initialization vector.
273	 */
274	protected function _vector() {
275		return Random::generate($this->_vectorSize());
276	}
277
278	/**
279	 * Returns the vector size.
280	 *
281	 * @return integer The vector size in bytes.
282	 */
283	protected function _vectorSize() {
284		return openssl_cipher_iv_length('aes-256-cbc');
285	}
286
287	/* Deprecated / BC */
288
289	/**
290	 * Indicates if we are in legacy / BC mode and the class is using the mcrypt extension
291	 * or we are able to use the openssl extension.
292	 *
293	 * @deprecated
294	 */
295	protected $_mcrypt = false;
296
297	/**
298	 * Holds the mcrypt crypto resource after initialization, when in legacy mode.
299	 *
300	 * @deprecated
301	 */
302	protected $_mcryptResource = null;
303
304	/**
305	 * Checks for legacy mode.
306	 *
307	 * @deprecated
308	 * @param array $config
309	 * @return boolean
310	 */
311	protected function _mcrypt(array $config) {
312		if (isset($config['cipher']) || isset($config['mode'])) {
313			$message  = "You've selected a non-default cipher and/or mode configuration. ";
314			$message .= "The Encrypt strategy is now in legacy mode and will use the ";
315			$message .= "deprecated mcrypt extension. To disable legacy mode, use the strategy ";
316			$message .= "with default configuration.";
317			trigger_error($message, E_USER_DEPRECATED);
318
319			return true;
320		}
321		if (!extension_loaded('openssl')) {
322			$message .= "The Encrypt strategy is now in legacy mode and will use the ";
323			$message .= "deprecated mcrypt extension. To disable legacy mode, install the ";
324			$message .= "openssl extension.";
325			trigger_error($message, E_USER_DEPRECATED);
326
327			return true;
328		}
329		return false;
330	}
331
332	/**
333	 * Destructor. Closes the crypto resource when it is no longer needed.
334	 *
335	 * @deprecated
336	 * @return void
337	 */
338	public function __destruct() {
339		if (is_resource($this->_mcryptResource)) {
340			mcrypt_module_close($this->_mcryptResource);
341		}
342	}
343
344	/**
345	 * Serialize and encrypt a given data array.
346	 *
347	 * @deprecated
348	 * @param array $decrypted The cleartext data to be encrypted.
349	 * @return string A Base64 encoded and encrypted string.
350	 */
351	protected function _bcEncrypt($decrypted = []) {
352		$vector = $this->_bcVector();
353		$secret = $this->_bcHashSecret($this->_config['secret']);
354
355		mcrypt_generic_init($this->_mcryptResource, $secret, $vector);
356		$encrypted = mcrypt_generic($this->_mcryptResource, serialize($decrypted));
357		mcrypt_generic_deinit($this->_mcryptResource);
358
359		return base64_encode($encrypted) . base64_encode($vector);
360	}
361
362	/**
363	 * Decrypt and unserialize a previously encrypted string.
364	 *
365	 * @deprecated
366	 * @param string $encrypted The base64 encoded and encrypted string.
367	 * @return array The cleartext data.
368	 */
369	protected function _bcDecrypt($encrypted) {
370		$secret = $this->_hashSecret($this->_config['secret']);
371
372		$vectorSize = strlen(base64_encode(str_repeat(" ", $this->_bcVectorSize())));
373		$vector = base64_decode(substr($encrypted, -$vectorSize));
374		$data = base64_decode(substr($encrypted, 0, -$vectorSize));
375
376		mcrypt_generic_init($this->_mcryptResource, $secret, $vector);
377		$decrypted = mdecrypt_generic($this->_mcryptResource, $data);
378		mcrypt_generic_deinit($this->_mcryptResource);
379
380		return unserialize(trim($decrypted));
381	}
382
383	/**
384	 * Hashes the given secret to make harder to detect.
385	 *
386	 * This method figures out the appropriate key size for the chosen encryption algorithm and
387	 * then hashes the given key accordingly. Note that if the key has already the needed length,
388	 * it is considered to be hashed (secure) already and is therefore not hashed again. This lets
389	 * you change the hashing method in your own code if you like.
390	 *
391	 * The default `MCRYPT_RIJNDAEL_128` key should be 32 byte long `sha256` is used
392	 * as the hashing algorithm. If the key size is shorter than the one generated by
393	 * `sha256`, the first n bytes will be used.
394	 *
395	 * @deprecated
396	 * @link http://php.net/function.mcrypt-enc-get-key-size.php
397	 * @param string $key The possibly too weak key.
398	 * @return string The hashed (raw) key.
399	 */
400	protected function _bcHashSecret($key) {
401		$size = mcrypt_enc_get_key_size($this->_mcryptResource);
402
403		if (strlen($key) >= $size) {
404			return $key;
405		}
406		return substr(hash('sha256', $key, true), 0, $size);
407	}
408
409	/**
410	 * Generates an initialization vector.
411	 *
412	 * @deprecated
413	 * @link http://php.net/function.mcrypt-create-iv.php
414	 * @return string Returns an initialization vector.
415	 */
416	protected function _bcVector() {
417		return mcrypt_create_iv($this->_bcVectorSize(), MCRYPT_DEV_URANDOM);
418	}
419
420	/**
421	 * Returns the vector size vor a given cipher and mode.
422	 *
423	 * @deprecated
424	 * @link http://php.net/function.mcrypt-enc-get-iv-size.php
425	 * @return number The vector size.
426	 */
427	protected function _bcVectorSize() {
428		return mcrypt_enc_get_iv_size($this->_mcryptResource);
429	}
430}
431
432?>