Encrypter.php 6.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. <?php
  2. namespace Illuminate\Encryption;
  3. use RuntimeException;
  4. use Illuminate\Contracts\Encryption\DecryptException;
  5. use Illuminate\Contracts\Encryption\EncryptException;
  6. use Illuminate\Contracts\Encryption\Encrypter as EncrypterContract;
  7. class Encrypter implements EncrypterContract
  8. {
  9. /**
  10. * The encryption key.
  11. *
  12. * @var string
  13. */
  14. protected $key;
  15. /**
  16. * The algorithm used for encryption.
  17. *
  18. * @var string
  19. */
  20. protected $cipher;
  21. /**
  22. * Create a new encrypter instance.
  23. *
  24. * @param string $key
  25. * @param string $cipher
  26. * @return void
  27. *
  28. * @throws \RuntimeException
  29. */
  30. public function __construct($key, $cipher = 'AES-128-CBC')
  31. {
  32. $key = (string) $key;
  33. if (static::supported($key, $cipher)) {
  34. $this->key = $key;
  35. $this->cipher = $cipher;
  36. } else {
  37. throw new RuntimeException('The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths.');
  38. }
  39. }
  40. /**
  41. * Determine if the given key and cipher combination is valid.
  42. *
  43. * @param string $key
  44. * @param string $cipher
  45. * @return bool
  46. */
  47. public static function supported($key, $cipher)
  48. {
  49. $length = mb_strlen($key, '8bit');
  50. return ($cipher === 'AES-128-CBC' && $length === 16) ||
  51. ($cipher === 'AES-256-CBC' && $length === 32);
  52. }
  53. /**
  54. * Create a new encryption key for the given cipher.
  55. *
  56. * @param string $cipher
  57. * @return string
  58. */
  59. public static function generateKey($cipher)
  60. {
  61. return random_bytes($cipher == 'AES-128-CBC' ? 16 : 32);
  62. }
  63. /**
  64. * Encrypt the given value.
  65. *
  66. * @param mixed $value
  67. * @param bool $serialize
  68. * @return string
  69. *
  70. * @throws \Illuminate\Contracts\Encryption\EncryptException
  71. */
  72. public function encrypt($value, $serialize = true)
  73. {
  74. $iv = random_bytes(openssl_cipher_iv_length($this->cipher));
  75. // First we will encrypt the value using OpenSSL. After this is encrypted we
  76. // will proceed to calculating a MAC for the encrypted value so that this
  77. // value can be verified later as not having been changed by the users.
  78. $value = \openssl_encrypt(
  79. $serialize ? serialize($value) : $value,
  80. $this->cipher, $this->key, 0, $iv
  81. );
  82. if ($value === false) {
  83. throw new EncryptException('Could not encrypt the data.');
  84. }
  85. // Once we get the encrypted value we'll go ahead and base64_encode the input
  86. // vector and create the MAC for the encrypted value so we can then verify
  87. // its authenticity. Then, we'll JSON the data into the "payload" array.
  88. $mac = $this->hash($iv = base64_encode($iv), $value);
  89. $json = json_encode(compact('iv', 'value', 'mac'));
  90. if (json_last_error() !== JSON_ERROR_NONE) {
  91. throw new EncryptException('Could not encrypt the data.');
  92. }
  93. return base64_encode($json);
  94. }
  95. /**
  96. * Encrypt a string without serialization.
  97. *
  98. * @param string $value
  99. * @return string
  100. */
  101. public function encryptString($value)
  102. {
  103. return $this->encrypt($value, false);
  104. }
  105. /**
  106. * Decrypt the given value.
  107. *
  108. * @param mixed $payload
  109. * @param bool $unserialize
  110. * @return string
  111. *
  112. * @throws \Illuminate\Contracts\Encryption\DecryptException
  113. */
  114. public function decrypt($payload, $unserialize = true)
  115. {
  116. $payload = $this->getJsonPayload($payload);
  117. $iv = base64_decode($payload['iv']);
  118. // Here we will decrypt the value. If we are able to successfully decrypt it
  119. // we will then unserialize it and return it out to the caller. If we are
  120. // unable to decrypt this value we will throw out an exception message.
  121. $decrypted = \openssl_decrypt(
  122. $payload['value'], $this->cipher, $this->key, 0, $iv
  123. );
  124. if ($decrypted === false) {
  125. throw new DecryptException('Could not decrypt the data.');
  126. }
  127. return $unserialize ? unserialize($decrypted) : $decrypted;
  128. }
  129. /**
  130. * Decrypt the given string without unserialization.
  131. *
  132. * @param string $payload
  133. * @return string
  134. */
  135. public function decryptString($payload)
  136. {
  137. return $this->decrypt($payload, false);
  138. }
  139. /**
  140. * Create a MAC for the given value.
  141. *
  142. * @param string $iv
  143. * @param mixed $value
  144. * @return string
  145. */
  146. protected function hash($iv, $value)
  147. {
  148. return hash_hmac('sha256', $iv.$value, $this->key);
  149. }
  150. /**
  151. * Get the JSON array from the given payload.
  152. *
  153. * @param string $payload
  154. * @return array
  155. *
  156. * @throws \Illuminate\Contracts\Encryption\DecryptException
  157. */
  158. protected function getJsonPayload($payload)
  159. {
  160. $payload = json_decode(base64_decode($payload), true);
  161. // If the payload is not valid JSON or does not have the proper keys set we will
  162. // assume it is invalid and bail out of the routine since we will not be able
  163. // to decrypt the given value. We'll also check the MAC for this encryption.
  164. if (! $this->validPayload($payload)) {
  165. throw new DecryptException('The payload is invalid.');
  166. }
  167. if (! $this->validMac($payload)) {
  168. throw new DecryptException('The MAC is invalid.');
  169. }
  170. return $payload;
  171. }
  172. /**
  173. * Verify that the encryption payload is valid.
  174. *
  175. * @param mixed $payload
  176. * @return bool
  177. */
  178. protected function validPayload($payload)
  179. {
  180. return is_array($payload) && isset($payload['iv'], $payload['value'], $payload['mac']) &&
  181. strlen(base64_decode($payload['iv'], true)) === openssl_cipher_iv_length($this->cipher);
  182. }
  183. /**
  184. * Determine if the MAC for the given payload is valid.
  185. *
  186. * @param array $payload
  187. * @return bool
  188. */
  189. protected function validMac(array $payload)
  190. {
  191. $calculated = $this->calculateMac($payload, $bytes = random_bytes(16));
  192. return hash_equals(
  193. hash_hmac('sha256', $payload['mac'], $bytes, true), $calculated
  194. );
  195. }
  196. /**
  197. * Calculate the hash of the given payload.
  198. *
  199. * @param array $payload
  200. * @param string $bytes
  201. * @return string
  202. */
  203. protected function calculateMac($payload, $bytes)
  204. {
  205. return hash_hmac(
  206. 'sha256', $this->hash($payload['iv'], $payload['value']), $bytes, true
  207. );
  208. }
  209. /**
  210. * Get the encryption key.
  211. *
  212. * @return string
  213. */
  214. public function getKey()
  215. {
  216. return $this->key;
  217. }
  218. }