PageRenderTime 55ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/classes/crypt.php

http://github.com/fuel/core
PHP | 636 lines | 291 code | 82 blank | 263 comment | 28 complexity | 0eb59f4b77f10d23f9edc53943597204 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * Fuel is a fast, lightweight, community driven PHP 5.4+ framework.
  4. *
  5. * @package Fuel
  6. * @version 1.9-dev
  7. * @author Fuel Development Team
  8. * @license MIT License
  9. * @copyright 2010 - 2019 Fuel Development Team
  10. * @link https://fuelphp.com
  11. */
  12. namespace Fuel\Core;
  13. use \phpseclib\Crypt\AES;
  14. use \phpseclib\Crypt\Hash;
  15. use \ParagonIE\Fuel\Binary;
  16. use \ParagonIE\Fuel\Base64UrlSafe;
  17. /**
  18. * Sodium encryption/decryption code based on HaLite from ParagonIE
  19. *
  20. * Copyright (c) 2016 - 2018 Paragon Initiative Enterprises.
  21. * Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
  22. */
  23. class Crypt
  24. {
  25. /**
  26. * Crypto default configuration
  27. *
  28. * @var array
  29. */
  30. protected static $defaults = array();
  31. /**
  32. * Defined Crypto instances
  33. *
  34. * @var array
  35. */
  36. protected static $_instances = array();
  37. /**
  38. * initialisation and auto configuration
  39. */
  40. public static function _init()
  41. {
  42. // load the ParagonIE classes we need
  43. import('paragonie.php', 'vendor');
  44. // load the config
  45. \Config::load('crypt', true);
  46. static::$defaults = \Config::get('crypt', array());
  47. // keep track of updates to the config
  48. $update = false;
  49. // check for legacy config
  50. if (empty(static::$defaults['legacy']))
  51. {
  52. $flag = true;
  53. foreach(array('crypto_key', 'crypto_iv', 'crypto_hmac') as $key)
  54. {
  55. if (empty(static::$defaults[$key]) or (strlen(static::$defaults[$key]) % 4) !== 0)
  56. {
  57. $flag = false;
  58. }
  59. }
  60. // and if we found something valid, convert it
  61. if ($flag)
  62. {
  63. static::$defaults['legacy'] = array();
  64. foreach(array('crypto_key', 'crypto_iv', 'crypto_hmac') as $key)
  65. {
  66. static::$defaults['legacy'][$key] = static::$defaults[$key];
  67. unset(static::$defaults[$key]);
  68. }
  69. $update = true;
  70. }
  71. }
  72. // check the sodium config
  73. if (empty(static::$defaults['sodium']['cipherkey']))
  74. {
  75. static::$defaults['sodium'] = array('cipherkey' => sodium_bin2hex(random_bytes(SODIUM_CRYPTO_STREAM_KEYBYTES)));
  76. $update = true;
  77. }
  78. // update the config if needed
  79. if ($update === true)
  80. {
  81. try
  82. {
  83. \Config::save('crypt', static::$defaults);
  84. }
  85. catch (\FileAccessException $e)
  86. {
  87. // failed to write the config file, inform the user
  88. echo \View::forge('errors/crypt_keys', array(
  89. 'keys' => static::$defaults,
  90. ));
  91. die();
  92. }
  93. }
  94. }
  95. /**
  96. * forge
  97. *
  98. * create a new named instance
  99. *
  100. * @param string $name instance name
  101. * @param array $config optional runtime configuration
  102. * @return \Crypt
  103. */
  104. public static function forge($name = '__default__', array $config = array())
  105. {
  106. if ( ! array_key_exists($name, static::$_instances))
  107. {
  108. static::$_instances[$name] = new static($config);
  109. }
  110. return static::$_instances[$name];
  111. }
  112. /**
  113. * Return a specific named instance
  114. *
  115. * @param string $name instance name
  116. * @return mixed Crypt if the instance exists, false if not
  117. */
  118. public static function instance($name = '__default__')
  119. {
  120. if ( ! array_key_exists($name, static::$_instances))
  121. {
  122. return static::forge($name);
  123. }
  124. return static::$_instances[$name];
  125. }
  126. /**
  127. * capture static calls to methods
  128. *
  129. * @param mixed $method
  130. * @param array $args The arguments will passed to $method.
  131. * @return mixed return value of $method.
  132. */
  133. public static function __callstatic($method, $args)
  134. {
  135. // static method calls are called on the default instance
  136. return call_user_func_array(array(static::instance(), $method), $args);
  137. }
  138. // --------------------------------------------------------------------
  139. /**
  140. * generate a URI safe base64 encoded string
  141. *
  142. * @param string $value
  143. * @return string
  144. */
  145. protected static function safe_b64encode($value)
  146. {
  147. $data = base64_encode($value);
  148. $data = str_replace(array('+', '/', '='), array('-', '_', ''), $data);
  149. return $data;
  150. }
  151. /**
  152. * decode a URI safe base64 encoded string
  153. *
  154. * @param string $value
  155. * @return string
  156. */
  157. protected static function safe_b64decode($value)
  158. {
  159. $data = str_replace(array('-', '_'), array('+', '/'), $value);
  160. $mod4 = strlen($data) % 4;
  161. if ($mod4)
  162. {
  163. $data .= substr('====', $mod4);
  164. }
  165. return base64_decode($data);
  166. }
  167. /**
  168. * compare two strings in a timing-insensitive way to prevent time-based attacks
  169. *
  170. * @param string $a
  171. * @param string $b
  172. * @return bool
  173. */
  174. protected static function secure_compare($a, $b)
  175. {
  176. // make sure we're only comparing equal length strings
  177. if (strlen($a) !== strlen($b))
  178. {
  179. return false;
  180. }
  181. // and that all comparisons take equal time
  182. $result = 0;
  183. for ($i = 0; $i < strlen($a); $i++)
  184. {
  185. $result |= ord($a[$i]) ^ ord($b[$i]);
  186. }
  187. return $result === 0;
  188. }
  189. /**
  190. * Split a key (using HKDF-BLAKE2b instead of HKDF-HMAC-*)
  191. *
  192. * @param string $key
  193. * @param string $salt
  194. * @return string[]
  195. */
  196. protected static function split_keys($key, $salt)
  197. {
  198. return array(
  199. static::hkdfBlake2b($key, SODIUM_CRYPTO_SECRETBOX_KEYBYTES, 'Halite|EncryptionKey', $salt),
  200. static::hkdfBlake2b($key, SODIUM_CRYPTO_AUTH_KEYBYTES, 'AuthenticationKeyFor_|Halite', $salt)
  201. );
  202. }
  203. /**
  204. * Split a message string into an array (assigned to variables via list()).
  205. *
  206. * Should return exactly 6 elements.
  207. *
  208. * @param string $ciphertext
  209. *
  210. * @return array<int, mixed>
  211. */
  212. protected static function split_message($message)
  213. {
  214. // get the message length
  215. $length = Binary::safeStrlen($message);
  216. // check ig it's long enough
  217. if ($length < 120)
  218. {
  219. throw new \FuelException('Crypt: Message is too short');
  220. }
  221. // the salt is used for key splitting (via HKDF)
  222. $salt = Binary::safeSubstr($message, 0, 32);
  223. // this is the nonce (we authenticated it)
  224. $nonce = Binary::safeSubstr($message, 32, SODIUM_CRYPTO_STREAM_NONCEBYTES);
  225. // This is the crypto_stream_xor()ed ciphertext
  226. $encrypted = Binary::safeSubstr($message, 56, $length - 120);
  227. // $auth is the last 32 bytes
  228. $auth = Binary::safeSubstr($message, $length - SODIUM_CRYPTO_GENERICHASH_BYTES_MAX);
  229. // We don't need this anymore.
  230. static::memzero($message);
  231. // Now we return the pieces in a specific order:
  232. return array($salt, $nonce, $encrypted, $auth);
  233. }
  234. /**
  235. * Use a derivative of HKDF to derive multiple keys from one.
  236. * http://tools.ietf.org/html/rfc5869
  237. *
  238. * This is a variant from hash_hkdf() and instead uses BLAKE2b provided by
  239. * libsodium.
  240. *
  241. * Important: instead of a true HKDF (from HMAC) construct, this uses the
  242. * crypto_generichash() key parameter. This is *probably* okay.
  243. *
  244. * @param string $ikm Initial Keying Material
  245. * @param int $length How many bytes?
  246. * @param string $info What sort of key are we deriving?
  247. * @param string $salt
  248. * @return string
  249. */
  250. protected static function hkdfBlake2b($ikm, $length, $info = '', $salt = '')
  251. {
  252. // Sanity-check the desired output length.
  253. if ($length < 0 or $length > (255 * SODIUM_CRYPTO_GENERICHASH_KEYBYTES))
  254. {
  255. throw new \FuelException('hkdfBlake2b Argument 2: Bad HKDF Digest Length');
  256. }
  257. // "If [salt] not provided, is set to a string of HashLen zeroes."
  258. if (empty($salt))
  259. {
  260. $salt = \str_repeat("\x00", SODIUM_CRYPTO_GENERICHASH_KEYBYTES);
  261. }
  262. // HKDF-Extract:
  263. // PRK = HMAC-Hash(salt, IKM)
  264. // The salt is the HMAC key.
  265. $prk = static::raw_keyed_hash($ikm, $salt);
  266. $t = '';
  267. $last_block = '';
  268. for ($block_index = 1; Binary::safeStrlen($t) < $length; ++$block_index)
  269. {
  270. // T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??)
  271. $last_block = static::raw_keyed_hash($last_block . $info . \chr($block_index), $prk);
  272. // T = T(1) | T(2) | T(3) | ... | T(N)
  273. $t .= $last_block;
  274. }
  275. // ORM = first L octets of T
  276. $orm = Binary::safeSubstr($t, 0, $length);
  277. return $orm;
  278. }
  279. /**
  280. * Wrapper around SODIUM_CRypto_generichash()
  281. *
  282. * Expects a key (binary string).
  283. * Returns raw binary.
  284. *
  285. * @param string $input
  286. * @param string $key
  287. * @param int $length
  288. * @return string
  289. */
  290. protected static function raw_keyed_hash($input, $key, $length = SODIUM_CRYPTO_GENERICHASH_BYTES)
  291. {
  292. if ($length < SODIUM_CRYPTO_GENERICHASH_BYTES_MIN)
  293. {
  294. throw new \FuelException(sprintf('Output length must be at least %d bytes.', SODIUM_CRYPTO_GENERICHASH_BYTES_MIN));
  295. }
  296. if ($length > SODIUM_CRYPTO_GENERICHASH_BYTES_MAX)
  297. {
  298. throw new \FuelException(sprintf('Output length must be at most %d bytes.', SODIUM_CRYPTO_GENERICHASH_BYTES_MAX));
  299. }
  300. return sodium_crypto_generichash($input, $key, $length);
  301. }
  302. /**
  303. * Calculate a MAC. This is used internally.
  304. *
  305. * @param string $message
  306. * @param string $authKey
  307. * @return string
  308. */
  309. protected static function calculate_mac($message, $auth_key)
  310. {
  311. return sodium_crypto_generichash($message, $auth_key, SODIUM_CRYPTO_GENERICHASH_BYTES_MAX);
  312. }
  313. /**
  314. * Verify a Message Authentication Code (MAC) of a message, with a shared
  315. * key.
  316. *
  317. * @param string $mac Message Authentication Code
  318. * @param string $message The message to verify
  319. * @param string $authKey Authentication key (symmetric)
  320. * @param SymmetricConfig $config Configuration object
  321. *
  322. * @return bool
  323. */
  324. protected static function verify_mac($mac, $message, $auth_key)
  325. {
  326. if (Binary::safeStrlen($mac) !== SODIUM_CRYPTO_GENERICHASH_BYTES_MAX)
  327. {
  328. throw new \FuelException('Crypt::verify_mac - Argument 1: Message Authentication Code is not the correct length; is it encoded?');
  329. }
  330. $calc = sodium_crypto_generichash($message, $auth_key, SODIUM_CRYPTO_GENERICHASH_BYTES_MAX);
  331. $res = Binary::hashEquals($mac, $calc);
  332. static::memzero($calc);
  333. return $res;
  334. }
  335. /**
  336. * Wrapper for sodium_memzero, it's actually not possible to zero
  337. * memory buffers in PHP. You need the native library for that.
  338. *
  339. * @param string|null $var
  340. *
  341. * @return void
  342. */
  343. protected static function memzero(&$var)
  344. {
  345. // check if we have native support
  346. if (PHP_VERSION_ID >= 70200 and extension_loaded('sodium'))
  347. {
  348. sodium_memzero($var);
  349. }
  350. elseif (extension_loaded('libsodium') and is_callable('\\Sodium\\memzero'))
  351. {
  352. @call_user_func('\\Sodium\\memzero', $var);
  353. }
  354. }
  355. // --------------------------------------------------------------------
  356. /**
  357. * Crypto object used to encrypt/decrypt
  358. *
  359. * @var object
  360. */
  361. protected $crypter = null;
  362. /**
  363. * Hash object used to generate hashes
  364. *
  365. * @var object
  366. */
  367. protected $hasher = null;
  368. /**
  369. * Crypto configuration
  370. *
  371. * @var array
  372. */
  373. protected $config = array();
  374. /**
  375. * Class constructor
  376. *
  377. * @param array $config
  378. */
  379. public function __construct(array $config = array())
  380. {
  381. $this->config = array_merge(static::$defaults, $config);
  382. // in case we need to decode legacy encrypted strings
  383. if ( ! empty($this->config['legacy']))
  384. {
  385. $this->legacy_crypter = new AES();
  386. $this->legacy_hasher = new Hash('sha256');
  387. $this->legacy_crypter->enableContinuousBuffer();
  388. $this->legacy_hasher->setKey(static::safe_b64decode($this->config['legacy']['crypto_hmac']));
  389. }
  390. }
  391. /**
  392. * capture calls to normal methods
  393. *
  394. * @param mixed $method
  395. * @param array $args The arguments will passed to $method.
  396. * @return mixed return value of $method.
  397. * @throws \ErrorException
  398. */
  399. public function __call($method, $args)
  400. {
  401. // validate the method called
  402. if ( ! in_array($method, array('encode', 'decode', 'legacy_decode')))
  403. {
  404. throw new \ErrorException('Call to undefined method '.__CLASS__.'::'.$method.'()', E_ERROR, 0, __FILE__, __LINE__);
  405. }
  406. // static method calls are called on the default instance
  407. return call_user_func_array(array($this, $method), $args);
  408. }
  409. /**
  410. * encrypt a string value, optionally with a custom key
  411. *
  412. * @param string $value value to encrypt
  413. * @param string|bool $key optional custom key to be used for this encryption
  414. * @param void $keylength no longer used
  415. * @return string encrypted value
  416. */
  417. protected function encode($value, $key = false, $keylength = false)
  418. {
  419. // get the binary key
  420. if ( ! $key)
  421. {
  422. $key = static::$defaults['sodium']['cipherkey'];
  423. }
  424. $key = sodium_hex2bin($key);
  425. // Generate a nonce and a HKDF salt
  426. $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
  427. $salt = random_bytes(32);
  428. /**
  429. * Split our key into two keys: One for encryption, the other for
  430. * authentication. By using separate keys, we can reasonably dismiss
  431. * likely cross-protocol attacks.
  432. *
  433. * This uses salted HKDF to split the keys, which is why we need the
  434. * salt in the first place.
  435. */
  436. list($enc_key, $auth_key) = static::split_keys($key, $salt);
  437. // Encrypt our message with the encryption key
  438. $encrypted = sodium_crypto_stream_xor($value, $nonce, $enc_key);
  439. static::memzero($enc_key);
  440. // Calculate an authentication tag
  441. $auth = static::calculate_mac($salt.$nonce.$encrypted, $auth_key);
  442. static::memzero($auth_key);
  443. // total encrypted message
  444. $message = $salt.$nonce.$encrypted.$auth;
  445. // wipe every superfluous piece of data from memory
  446. static::memzero($nonce);
  447. static::memzero($salt);
  448. static::memzero($encrypted);
  449. static::memzero($auth);
  450. // return the base64 encoded message
  451. return 'S:'.Base64UrlSafe::encode($message);
  452. }
  453. /**
  454. * decrypt a string value, optionally with a custom key
  455. *
  456. * @param string $value value to decrypt
  457. * @param string|bool $key optional custom key to be used for this encryption
  458. * @param void $keylength no longer used
  459. * @access public
  460. * @return string encrypted value
  461. */
  462. protected function decode($value, $key = false, $keylength = false)
  463. {
  464. // legacy or sodium value?
  465. $value = explode('S:', $value);
  466. if ( ! isset($value[1]))
  467. {
  468. // decode using the legacy method
  469. return $this->legacy_decode($value[0], $key, $keylength);
  470. }
  471. $value = $value[1];
  472. // get the binary key
  473. if ( ! $key)
  474. {
  475. $key = static::$defaults['sodium']['cipherkey'];
  476. }
  477. $key = sodium_hex2bin($key);
  478. // get the base64 decoded message
  479. $value = Base64UrlSafe::decode($value);
  480. // split the message into it's components
  481. list ($salt, $nonce, $encrypted, $auth) = static::split_message($value);
  482. /* Split our key into two keys: One for encryption, the other for
  483. * authentication. By using separate keys, we can reasonably dismiss
  484. * likely cross-protocol attacks.
  485. *
  486. * This uses salted HKDF to split the keys, which is why we need the
  487. * salt in the first place.
  488. */
  489. list($enc_key, $auth_key) = static::split_keys($key, $salt);
  490. // Check the MAC first
  491. $res = static::verify_mac($auth, $salt.$nonce.$encrypted, $auth_key);
  492. static::memzero($salt);
  493. static::memzero($auth_key);
  494. if ($res)
  495. {
  496. // crypto_stream_xor() can be used to encrypt and decrypt
  497. /** @var string $plaintext */
  498. $message = sodium_crypto_stream_xor($encrypted, $nonce, $enc_key);
  499. }
  500. static::memzero($encrypted);
  501. static::memzero($nonce);
  502. static::memzero($enc_key);
  503. return $res ? $message : false;
  504. }
  505. /**
  506. * decrypt a string value, optionally with a custom key
  507. *
  508. * @param string $value value to decrypt
  509. * @param string|bool $key optional custom key to be used for this encryption
  510. * @param int|bool $keylength optional key length
  511. * @access public
  512. * @return string encrypted value
  513. */
  514. protected function legacy_decode($value, $key = false, $keylength = false)
  515. {
  516. // make sure we have legacy keys
  517. if (empty($this->config['legacy']['crypto_key']))
  518. {
  519. throw new \FuelException('Can not decode this string, no legacy crypt keys defined');
  520. }
  521. if ( ! $key)
  522. {
  523. $key = static::safe_b64decode($this->config['legacy']['crypto_key']);
  524. // Used for backwards compatibility with encrypted data prior
  525. // to FuelPHP 1.7.2, when phpseclib was updated, and became a
  526. // bit smarter about figuring out key lengths.
  527. $keylength = 128;
  528. }
  529. if ($keylength)
  530. {
  531. $this->legacy_crypter->setKeyLength($keylength);
  532. }
  533. $this->legacy_crypter->setKey($key);
  534. $this->legacy_crypter->setIV(static::safe_b64decode($this->config['legacy']['crypto_iv']));
  535. $value = static::safe_b64decode($value);
  536. if ($value = $this->validate_hmac($value))
  537. {
  538. return $this->legacy_crypter->decrypt($value);
  539. }
  540. else
  541. {
  542. return false;
  543. }
  544. }
  545. protected function validate_hmac($value)
  546. {
  547. // strip the hmac-sha256 hash from the value
  548. $hmac = substr($value, strlen($value)-43);
  549. // and remove it from the value
  550. $value = substr($value, 0, strlen($value)-43);
  551. // only return the value if it wasn't tampered with
  552. return (static::secure_compare(static::safe_b64encode($this->legacy_hasher->hash($value)), $hmac)) ? $value : false;
  553. }
  554. }