DecryptionTraitV2.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. <?php
  2. namespace Aws\Crypto;
  3. use Aws\Exception\CryptoException;
  4. use GuzzleHttp\Psr7;
  5. use GuzzleHttp\Psr7\LimitStream;
  6. use Psr\Http\Message\StreamInterface;
  7. trait DecryptionTraitV2
  8. {
  9. /**
  10. * Dependency to reverse lookup the openssl_* cipher name from the AESName
  11. * in the MetadataEnvelope.
  12. *
  13. * @param $aesName
  14. *
  15. * @return string
  16. *
  17. * @internal
  18. */
  19. abstract protected function getCipherFromAesName($aesName);
  20. /**
  21. * Dependency to generate a CipherMethod from a set of inputs for loading
  22. * in to an AesDecryptingStream.
  23. *
  24. * @param string $cipherName Name of the cipher to generate for decrypting.
  25. * @param string $iv Base Initialization Vector for the cipher.
  26. * @param int $keySize Size of the encryption key, in bits, that will be
  27. * used.
  28. *
  29. * @return Cipher\CipherMethod
  30. *
  31. * @internal
  32. */
  33. abstract protected function buildCipherMethod($cipherName, $iv, $keySize);
  34. /**
  35. * Builds an AesStreamInterface using cipher options loaded from the
  36. * MetadataEnvelope and MaterialsProvider. Can decrypt data from both the
  37. * legacy and V2 encryption client workflows.
  38. *
  39. * @param string $cipherText Plain-text data to be encrypted using the
  40. * materials, algorithm, and data provided.
  41. * @param MaterialsProviderInterfaceV2 $provider A provider to supply and encrypt
  42. * materials used in encryption.
  43. * @param MetadataEnvelope $envelope A storage envelope for encryption
  44. * metadata to be read from.
  45. * @param array $options Options used for decryption.
  46. *
  47. * @return AesStreamInterface
  48. *
  49. * @throws \InvalidArgumentException Thrown when a value in $cipherOptions
  50. * is not valid.
  51. *
  52. * @internal
  53. */
  54. public function decrypt(
  55. $cipherText,
  56. MaterialsProviderInterfaceV2 $provider,
  57. MetadataEnvelope $envelope,
  58. array $options = []
  59. ) {
  60. $options['@CipherOptions'] = !empty($options['@CipherOptions'])
  61. ? $options['@CipherOptions']
  62. : [];
  63. $options['@CipherOptions']['Iv'] = base64_decode(
  64. $envelope[MetadataEnvelope::IV_HEADER]
  65. );
  66. $options['@CipherOptions']['TagLength'] =
  67. $envelope[MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER] / 8;
  68. $cek = $provider->decryptCek(
  69. base64_decode(
  70. $envelope[MetadataEnvelope::CONTENT_KEY_V2_HEADER]
  71. ),
  72. json_decode(
  73. $envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER],
  74. true
  75. ),
  76. $options
  77. );
  78. $options['@CipherOptions']['KeySize'] = strlen($cek) * 8;
  79. $options['@CipherOptions']['Cipher'] = $this->getCipherFromAesName(
  80. $envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER]
  81. );
  82. $this->validateOptionsAndEnvelope($options, $envelope);
  83. $decryptionStream = $this->getDecryptingStream(
  84. $cipherText,
  85. $cek,
  86. $options['@CipherOptions']
  87. );
  88. unset($cek);
  89. return $decryptionStream;
  90. }
  91. private function getTagFromCiphertextStream(
  92. StreamInterface $cipherText,
  93. $tagLength
  94. ) {
  95. $cipherTextSize = $cipherText->getSize();
  96. if ($cipherTextSize == null || $cipherTextSize <= 0) {
  97. throw new \RuntimeException('Cannot decrypt a stream of unknown'
  98. . ' size.');
  99. }
  100. return (string) new LimitStream(
  101. $cipherText,
  102. $tagLength,
  103. $cipherTextSize - $tagLength
  104. );
  105. }
  106. private function getStrippedCiphertextStream(
  107. StreamInterface $cipherText,
  108. $tagLength
  109. ) {
  110. $cipherTextSize = $cipherText->getSize();
  111. if ($cipherTextSize == null || $cipherTextSize <= 0) {
  112. throw new \RuntimeException('Cannot decrypt a stream of unknown'
  113. . ' size.');
  114. }
  115. return new LimitStream(
  116. $cipherText,
  117. $cipherTextSize - $tagLength,
  118. 0
  119. );
  120. }
  121. private function validateOptionsAndEnvelope($options, $envelope)
  122. {
  123. $allowedCiphers = AbstractCryptoClientV2::$supportedCiphers;
  124. $allowedKeywraps = AbstractCryptoClientV2::$supportedKeyWraps;
  125. if ($options['@SecurityProfile'] == 'V2_AND_LEGACY') {
  126. $allowedCiphers = array_unique(array_merge(
  127. $allowedCiphers,
  128. AbstractCryptoClient::$supportedCiphers
  129. ));
  130. $allowedKeywraps = array_unique(array_merge(
  131. $allowedKeywraps,
  132. AbstractCryptoClient::$supportedKeyWraps
  133. ));
  134. }
  135. $v1SchemaException = new CryptoException("The requested object is encrypted"
  136. . " with V1 encryption schemas that have been disabled by"
  137. . " client configuration @SecurityProfile=V2. Retry with"
  138. . " V2_AND_LEGACY enabled or reencrypt the object.");
  139. if (!in_array($options['@CipherOptions']['Cipher'], $allowedCiphers)) {
  140. if (in_array($options['@CipherOptions']['Cipher'], AbstractCryptoClient::$supportedCiphers)) {
  141. throw $v1SchemaException;
  142. }
  143. throw new CryptoException("The requested object is encrypted with"
  144. . " the cipher '{$options['@CipherOptions']['Cipher']}', which is not"
  145. . " supported for decryption with the selected security profile."
  146. . " This profile allows decryption with: "
  147. . implode(", ", $allowedCiphers));
  148. }
  149. if (!in_array(
  150. $envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER],
  151. $allowedKeywraps
  152. )) {
  153. if (in_array(
  154. $envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER],
  155. AbstractCryptoClient::$supportedKeyWraps)
  156. ) {
  157. throw $v1SchemaException;
  158. }
  159. throw new CryptoException("The requested object is encrypted with"
  160. . " the keywrap schema '{$envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER]}',"
  161. . " which is not supported for decryption with the current security"
  162. . " profile.");
  163. }
  164. $matdesc = json_decode(
  165. $envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER],
  166. true
  167. );
  168. if (isset($matdesc['aws:x-amz-cek-alg'])
  169. && $envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER] !==
  170. $matdesc['aws:x-amz-cek-alg']
  171. ) {
  172. throw new CryptoException("There is a mismatch in specified content"
  173. . " encryption algrithm between the materials description value"
  174. . " and the metadata envelope value: {$matdesc['aws:x-amz-cek-alg']}"
  175. . " vs. {$envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER]}.");
  176. }
  177. }
  178. /**
  179. * Generates a stream that wraps the cipher text with the proper cipher and
  180. * uses the content encryption key (CEK) to decrypt the data when read.
  181. *
  182. * @param string $cipherText Plain-text data to be encrypted using the
  183. * materials, algorithm, and data provided.
  184. * @param string $cek A content encryption key for use by the stream for
  185. * encrypting the plaintext data.
  186. * @param array $cipherOptions Options for use in determining the cipher to
  187. * be used for encrypting data.
  188. *
  189. * @return AesStreamInterface
  190. *
  191. * @internal
  192. */
  193. protected function getDecryptingStream(
  194. $cipherText,
  195. $cek,
  196. $cipherOptions
  197. ) {
  198. $cipherTextStream = Psr7\Utils::streamFor($cipherText);
  199. switch ($cipherOptions['Cipher']) {
  200. case 'gcm':
  201. $cipherOptions['Tag'] = $this->getTagFromCiphertextStream(
  202. $cipherTextStream,
  203. $cipherOptions['TagLength']
  204. );
  205. return new AesGcmDecryptingStream(
  206. $this->getStrippedCiphertextStream(
  207. $cipherTextStream,
  208. $cipherOptions['TagLength']
  209. ),
  210. $cek,
  211. $cipherOptions['Iv'],
  212. $cipherOptions['Tag'],
  213. $cipherOptions['Aad'] = isset($cipherOptions['Aad'])
  214. ? $cipherOptions['Aad']
  215. : '',
  216. $cipherOptions['TagLength'] ?: null,
  217. $cipherOptions['KeySize']
  218. );
  219. default:
  220. $cipherMethod = $this->buildCipherMethod(
  221. $cipherOptions['Cipher'],
  222. $cipherOptions['Iv'],
  223. $cipherOptions['KeySize']
  224. );
  225. return new AesDecryptingStream(
  226. $cipherTextStream,
  227. $cek,
  228. $cipherMethod
  229. );
  230. }
  231. }
  232. }