AesGcm.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. <?php
  2. namespace Aws\Crypto\Polyfill;
  3. use Aws\Exception\CryptoPolyfillException;
  4. use InvalidArgumentException;
  5. use RangeException;
  6. /**
  7. * Class AesGcm
  8. *
  9. * This provides a polyfill for AES-GCM encryption/decryption, with caveats:
  10. *
  11. * 1. Only 96-bit nonces are supported.
  12. * 2. Only 128-bit authentication tags are supported. (i.e. non-truncated)
  13. *
  14. * Supports AES key sizes of 128-bit, 192-bit, and 256-bit.
  15. */
  16. class AesGcm
  17. {
  18. use NeedsTrait;
  19. /** @var Key $aesKey */
  20. private $aesKey;
  21. /** @var int $keySize */
  22. private $keySize;
  23. /** @var int $blockSize */
  24. protected $blockSize = 8192;
  25. /**
  26. * AesGcm constructor.
  27. *
  28. * @param Key $aesKey
  29. * @param int $keySize
  30. * @param int $blockSize
  31. *
  32. * @throws CryptoPolyfillException
  33. * @throws InvalidArgumentException
  34. * @throws RangeException
  35. */
  36. public function __construct(Key $aesKey, $keySize = 256, $blockSize = 8192)
  37. {
  38. /* Preconditions: */
  39. self::needs(
  40. \in_array($keySize, [128, 192, 256], true),
  41. "Key size must be 128, 192, or 256 bits; {$keySize} given",
  42. InvalidArgumentException::class
  43. );
  44. self::needs(
  45. \is_int($blockSize) && $blockSize > 0 && $blockSize <= PHP_INT_MAX,
  46. 'Block size must be a positive integer.',
  47. RangeException::class
  48. );
  49. self::needs(
  50. $aesKey->length() << 3 === $keySize,
  51. 'Incorrect key size; expected ' . $keySize . ' bits, got ' . ($aesKey->length() << 3) . ' bits.'
  52. );
  53. $this->aesKey = $aesKey;
  54. $this->keySize = $keySize;
  55. }
  56. /**
  57. * Encryption interface for AES-GCM
  58. *
  59. * @param string $plaintext Message to be encrypted
  60. * @param string $nonce Number to be used ONCE
  61. * @param Key $key AES Key
  62. * @param string $aad Additional authenticated data
  63. * @param string &$tag Reference to variable to hold tag
  64. * @param int $keySize Key size (bits)
  65. * @param int $blockSize Block size (bytes) -- How much memory to buffer
  66. * @return string
  67. * @throws InvalidArgumentException
  68. */
  69. public static function encrypt(
  70. $plaintext,
  71. $nonce,
  72. Key $key,
  73. $aad,
  74. &$tag,
  75. $keySize = 256,
  76. $blockSize = 8192
  77. ) {
  78. self::needs(
  79. self::strlen($nonce) === 12,
  80. 'Nonce must be exactly 12 bytes',
  81. InvalidArgumentException::class
  82. );
  83. $encryptor = new AesGcm($key, $keySize, $blockSize);
  84. list($aadLength, $gmac) = $encryptor->gmacInit($nonce, $aad);
  85. $ciphertext = \openssl_encrypt(
  86. $plaintext,
  87. "aes-{$encryptor->keySize}-ctr",
  88. $key->get(),
  89. OPENSSL_NO_PADDING | OPENSSL_RAW_DATA,
  90. $nonce . "\x00\x00\x00\x02"
  91. );
  92. /* Calculate auth tag in a streaming fashion to minimize memory usage: */
  93. $ciphertextLength = self::strlen($ciphertext);
  94. for ($i = 0; $i < $ciphertextLength; $i += $encryptor->blockSize) {
  95. $cBlock = new ByteArray(self::substr($ciphertext, $i, $encryptor->blockSize));
  96. $gmac->update($cBlock);
  97. }
  98. $tag = $gmac->finish($aadLength, $ciphertextLength)->toString();
  99. return $ciphertext;
  100. }
  101. /**
  102. * Decryption interface for AES-GCM
  103. *
  104. * @param string $ciphertext Ciphertext to decrypt
  105. * @param string $nonce Number to be used ONCE
  106. * @param Key $key AES key
  107. * @param string $aad Additional authenticated data
  108. * @param string $tag Authentication tag
  109. * @param int $keySize Key size (bits)
  110. * @param int $blockSize Block size (bytes) -- How much memory to buffer
  111. * @return string Plaintext
  112. *
  113. * @throws CryptoPolyfillException
  114. * @throws InvalidArgumentException
  115. */
  116. public static function decrypt(
  117. $ciphertext,
  118. $nonce,
  119. Key $key,
  120. $aad,
  121. &$tag,
  122. $keySize = 256,
  123. $blockSize = 8192
  124. ) {
  125. /* Precondition: */
  126. self::needs(
  127. self::strlen($nonce) === 12,
  128. 'Nonce must be exactly 12 bytes',
  129. InvalidArgumentException::class
  130. );
  131. $encryptor = new AesGcm($key, $keySize, $blockSize);
  132. list($aadLength, $gmac) = $encryptor->gmacInit($nonce, $aad);
  133. /* Calculate auth tag in a streaming fashion to minimize memory usage: */
  134. $ciphertextLength = self::strlen($ciphertext);
  135. for ($i = 0; $i < $ciphertextLength; $i += $encryptor->blockSize) {
  136. $cBlock = new ByteArray(self::substr($ciphertext, $i, $encryptor->blockSize));
  137. $gmac->update($cBlock);
  138. }
  139. /* Validate auth tag in constant-time: */
  140. $calc = $gmac->finish($aadLength, $ciphertextLength);
  141. $expected = new ByteArray($tag);
  142. self::needs($calc->equals($expected), 'Invalid authentication tag');
  143. /* Return plaintext if auth tag check succeeded: */
  144. return \openssl_decrypt(
  145. $ciphertext,
  146. "aes-{$encryptor->keySize}-ctr",
  147. $key->get(),
  148. OPENSSL_NO_PADDING | OPENSSL_RAW_DATA,
  149. $nonce . "\x00\x00\x00\x02"
  150. );
  151. }
  152. /**
  153. * Initialize a Gmac object with the nonce and this object's key.
  154. *
  155. * @param string $nonce Must be exactly 12 bytes long.
  156. * @param string|null $aad
  157. * @return array
  158. */
  159. protected function gmacInit($nonce, $aad = null)
  160. {
  161. $gmac = new Gmac(
  162. $this->aesKey,
  163. $nonce . "\x00\x00\x00\x01",
  164. $this->keySize
  165. );
  166. $aadBlock = new ByteArray($aad);
  167. $aadLength = $aadBlock->count();
  168. $gmac->update($aadBlock);
  169. $gmac->flush();
  170. return [$aadLength, $gmac];
  171. }
  172. /**
  173. * Calculate the length of a string.
  174. *
  175. * Uses the appropriate PHP function without being brittle to
  176. * mbstring.func_overload.
  177. *
  178. * @param string $string
  179. * @return int
  180. */
  181. protected static function strlen($string)
  182. {
  183. if (\is_callable('\\mb_strlen')) {
  184. return (int) \mb_strlen($string, '8bit');
  185. }
  186. return (int) \strlen($string);
  187. }
  188. /**
  189. * Return a substring of the provided string.
  190. *
  191. * Uses the appropriate PHP function without being brittle to
  192. * mbstring.func_overload.
  193. *
  194. * @param string $string
  195. * @param int $offset
  196. * @param int|null $length
  197. * @return string
  198. */
  199. protected static function substr($string, $offset = 0, $length = null)
  200. {
  201. if (\is_callable('\\mb_substr')) {
  202. return \mb_substr($string, $offset, $length, '8bit');
  203. } elseif (!\is_null($length)) {
  204. return \substr($string, $offset, $length);
  205. }
  206. return \substr($string, $offset);
  207. }
  208. }