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