S3EncryptionClientV2.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. <?php
  2. namespace Aws\S3\Crypto;
  3. use Aws\Crypto\DecryptionTraitV2;
  4. use Aws\Exception\CryptoException;
  5. use Aws\HashingStream;
  6. use Aws\PhpHash;
  7. use Aws\Crypto\AbstractCryptoClientV2;
  8. use Aws\Crypto\EncryptionTraitV2;
  9. use Aws\Crypto\MetadataEnvelope;
  10. use Aws\Crypto\MaterialsProvider;
  11. use Aws\Crypto\Cipher\CipherBuilderTrait;
  12. use Aws\S3\S3Client;
  13. use GuzzleHttp\Promise;
  14. use GuzzleHttp\Promise\PromiseInterface;
  15. use GuzzleHttp\Psr7;
  16. /**
  17. * Provides a wrapper for an S3Client that supplies functionality to encrypt
  18. * data on putObject[Async] calls and decrypt data on getObject[Async] calls.
  19. *
  20. * AWS strongly recommends the upgrade to the S3EncryptionClientV2 (over the
  21. * S3EncryptionClient), as it offers updated data security best practices to our
  22. * customers who upgrade. S3EncryptionClientV2 contains breaking changes, so this
  23. * will require planning by engineering teams to migrate. New workflows should
  24. * just start with S3EncryptionClientV2.
  25. *
  26. * Note that for PHP versions of < 7.1, this class uses an AES-GCM polyfill
  27. * for encryption since there is no native PHP support. The performance for large
  28. * inputs will be a lot slower than for PHP 7.1+, so upgrading older PHP version
  29. * environments may be necessary to use this effectively.
  30. *
  31. * Example write path:
  32. *
  33. * <code>
  34. * use Aws\Crypto\KmsMaterialsProviderV2;
  35. * use Aws\S3\Crypto\S3EncryptionClientV2;
  36. * use Aws\S3\S3Client;
  37. *
  38. * $encryptionClient = new S3EncryptionClientV2(
  39. * new S3Client([
  40. * 'region' => 'us-west-2',
  41. * 'version' => 'latest'
  42. * ])
  43. * );
  44. * $materialsProvider = new KmsMaterialsProviderV2(
  45. * new KmsClient([
  46. * 'profile' => 'default',
  47. * 'region' => 'us-east-1',
  48. * 'version' => 'latest',
  49. * ],
  50. * 'your-kms-key-id'
  51. * );
  52. *
  53. * $encryptionClient->putObject([
  54. * '@MaterialsProvider' => $materialsProvider,
  55. * '@CipherOptions' => [
  56. * 'Cipher' => 'gcm',
  57. * 'KeySize' => 256,
  58. * ],
  59. * '@KmsEncryptionContext' => ['foo' => 'bar'],
  60. * 'Bucket' => 'your-bucket',
  61. * 'Key' => 'your-key',
  62. * 'Body' => 'your-encrypted-data',
  63. * ]);
  64. * </code>
  65. *
  66. * Example read call (using objects from previous example):
  67. *
  68. * <code>
  69. * $encryptionClient->getObject([
  70. * '@MaterialsProvider' => $materialsProvider,
  71. * '@CipherOptions' => [
  72. * 'Cipher' => 'gcm',
  73. * 'KeySize' => 256,
  74. * ],
  75. * 'Bucket' => 'your-bucket',
  76. * 'Key' => 'your-key',
  77. * ]);
  78. * </code>
  79. */
  80. class S3EncryptionClientV2 extends AbstractCryptoClientV2
  81. {
  82. use CipherBuilderTrait;
  83. use CryptoParamsTraitV2;
  84. use DecryptionTraitV2;
  85. use EncryptionTraitV2;
  86. use UserAgentTrait;
  87. const CRYPTO_VERSION = '2.1';
  88. private $client;
  89. private $instructionFileSuffix;
  90. private $legacyWarningCount;
  91. /**
  92. * @param S3Client $client The S3Client to be used for true uploading and
  93. * retrieving objects from S3 when using the
  94. * encryption client.
  95. * @param string|null $instructionFileSuffix Suffix for a client wide
  96. * default when using instruction
  97. * files for metadata storage.
  98. */
  99. public function __construct(
  100. S3Client $client,
  101. $instructionFileSuffix = null
  102. ) {
  103. $this->appendUserAgent($client, 'feat/s3-encrypt/' . self::CRYPTO_VERSION);
  104. $this->client = $client;
  105. $this->instructionFileSuffix = $instructionFileSuffix;
  106. $this->legacyWarningCount = 0;
  107. }
  108. private static function getDefaultStrategy()
  109. {
  110. return new HeadersMetadataStrategy();
  111. }
  112. /**
  113. * Encrypts the data in the 'Body' field of $args and promises to upload it
  114. * to the specified location on S3.
  115. *
  116. * Note that for PHP versions of < 7.1, this operation uses an AES-GCM
  117. * polyfill for encryption since there is no native PHP support. The
  118. * performance for large inputs will be a lot slower than for PHP 7.1+, so
  119. * upgrading older PHP version environments may be necessary to use this
  120. * effectively.
  121. *
  122. * @param array $args Arguments for encrypting an object and uploading it
  123. * to S3 via PutObject.
  124. *
  125. * The required configuration arguments are as follows:
  126. *
  127. * - @MaterialsProvider: (MaterialsProviderV2) Provides Cek, Iv, and Cek
  128. * encrypting/decrypting for encryption metadata.
  129. * - @CipherOptions: (array) Cipher options for encrypting data. Only the
  130. * Cipher option is required. Accepts the following:
  131. * - Cipher: (string) gcm
  132. * See also: AbstractCryptoClientV2::$supportedCiphers
  133. * - KeySize: (int) 128|256
  134. * See also: MaterialsProvider::$supportedKeySizes
  135. * - Aad: (string) Additional authentication data. This option is
  136. * passed directly to OpenSSL when using gcm. Note if you pass in
  137. * Aad, the PHP SDK will be able to decrypt the resulting object,
  138. * but other AWS SDKs may not be able to do so.
  139. * - @KmsEncryptionContext: (array) Only required if using
  140. * KmsMaterialsProviderV2. An associative array of key-value
  141. * pairs to be added to the encryption context for KMS key encryption. An
  142. * empty array may be passed if no additional context is desired.
  143. *
  144. * The optional configuration arguments are as follows:
  145. *
  146. * - @MetadataStrategy: (MetadataStrategy|string|null) Strategy for storing
  147. * MetadataEnvelope information. Defaults to using a
  148. * HeadersMetadataStrategy. Can either be a class implementing
  149. * MetadataStrategy, a class name of a predefined strategy, or empty/null
  150. * to default.
  151. * - @InstructionFileSuffix: (string|null) Suffix used when writing to an
  152. * instruction file if using an InstructionFileMetadataHandler.
  153. *
  154. * @return PromiseInterface
  155. *
  156. * @throws \InvalidArgumentException Thrown when arguments above are not
  157. * passed or are passed incorrectly.
  158. */
  159. public function putObjectAsync(array $args)
  160. {
  161. $provider = $this->getMaterialsProvider($args);
  162. unset($args['@MaterialsProvider']);
  163. $instructionFileSuffix = $this->getInstructionFileSuffix($args);
  164. unset($args['@InstructionFileSuffix']);
  165. $strategy = $this->getMetadataStrategy($args, $instructionFileSuffix);
  166. unset($args['@MetadataStrategy']);
  167. $envelope = new MetadataEnvelope();
  168. return Promise\Create::promiseFor($this->encrypt(
  169. Psr7\Utils::streamFor($args['Body']),
  170. $args,
  171. $provider,
  172. $envelope
  173. ))->then(
  174. function ($encryptedBodyStream) use ($args) {
  175. $hash = new PhpHash('sha256');
  176. $hashingEncryptedBodyStream = new HashingStream(
  177. $encryptedBodyStream,
  178. $hash,
  179. self::getContentShaDecorator($args)
  180. );
  181. return [$hashingEncryptedBodyStream, $args];
  182. }
  183. )->then(
  184. function ($putObjectContents) use ($strategy, $envelope) {
  185. list($bodyStream, $args) = $putObjectContents;
  186. if ($strategy === null) {
  187. $strategy = self::getDefaultStrategy();
  188. }
  189. $updatedArgs = $strategy->save($envelope, $args);
  190. $updatedArgs['Body'] = $bodyStream;
  191. return $updatedArgs;
  192. }
  193. )->then(
  194. function ($args) {
  195. unset($args['@CipherOptions']);
  196. return $this->client->putObjectAsync($args);
  197. }
  198. );
  199. }
  200. private static function getContentShaDecorator(&$args)
  201. {
  202. return function ($hash) use (&$args) {
  203. $args['ContentSHA256'] = bin2hex($hash);
  204. };
  205. }
  206. /**
  207. * Encrypts the data in the 'Body' field of $args and uploads it to the
  208. * specified location on S3.
  209. *
  210. * Note that for PHP versions of < 7.1, this operation uses an AES-GCM
  211. * polyfill for encryption since there is no native PHP support. The
  212. * performance for large inputs will be a lot slower than for PHP 7.1+, so
  213. * upgrading older PHP version environments may be necessary to use this
  214. * effectively.
  215. *
  216. * @param array $args Arguments for encrypting an object and uploading it
  217. * to S3 via PutObject.
  218. *
  219. * The required configuration arguments are as follows:
  220. *
  221. * - @MaterialsProvider: (MaterialsProvider) Provides Cek, Iv, and Cek
  222. * encrypting/decrypting for encryption metadata.
  223. * - @CipherOptions: (array) Cipher options for encrypting data. A Cipher
  224. * is required. Accepts the following options:
  225. * - Cipher: (string) gcm
  226. * See also: AbstractCryptoClientV2::$supportedCiphers
  227. * - KeySize: (int) 128|256
  228. * See also: MaterialsProvider::$supportedKeySizes
  229. * - Aad: (string) Additional authentication data. This option is
  230. * passed directly to OpenSSL when using gcm. Note if you pass in
  231. * Aad, the PHP SDK will be able to decrypt the resulting object,
  232. * but other AWS SDKs may not be able to do so.
  233. * - @KmsEncryptionContext: (array) Only required if using
  234. * KmsMaterialsProviderV2. An associative array of key-value
  235. * pairs to be added to the encryption context for KMS key encryption. An
  236. * empty array may be passed if no additional context is desired.
  237. *
  238. * The optional configuration arguments are as follows:
  239. *
  240. * - @MetadataStrategy: (MetadataStrategy|string|null) Strategy for storing
  241. * MetadataEnvelope information. Defaults to using a
  242. * HeadersMetadataStrategy. Can either be a class implementing
  243. * MetadataStrategy, a class name of a predefined strategy, or empty/null
  244. * to default.
  245. * - @InstructionFileSuffix: (string|null) Suffix used when writing to an
  246. * instruction file if an using an InstructionFileMetadataHandler was
  247. * determined.
  248. *
  249. * @return \Aws\Result PutObject call result with the details of uploading
  250. * the encrypted file.
  251. *
  252. * @throws \InvalidArgumentException Thrown when arguments above are not
  253. * passed or are passed incorrectly.
  254. */
  255. public function putObject(array $args)
  256. {
  257. return $this->putObjectAsync($args)->wait();
  258. }
  259. /**
  260. * Promises to retrieve an object from S3 and decrypt the data in the
  261. * 'Body' field.
  262. *
  263. * @param array $args Arguments for retrieving an object from S3 via
  264. * GetObject and decrypting it.
  265. *
  266. * The required configuration argument is as follows:
  267. *
  268. * - @MaterialsProvider: (MaterialsProviderInterface) Provides Cek, Iv, and Cek
  269. * encrypting/decrypting for decryption metadata. May have data loaded
  270. * from the MetadataEnvelope upon decryption.
  271. * - @SecurityProfile: (string) Must be set to 'V2' or 'V2_AND_LEGACY'.
  272. * - 'V2' indicates that only objects encrypted with S3EncryptionClientV2
  273. * content encryption and key wrap schemas are able to be decrypted.
  274. * - 'V2_AND_LEGACY' indicates that objects encrypted with both
  275. * S3EncryptionClientV2 and older legacy encryption clients are able
  276. * to be decrypted.
  277. *
  278. * The optional configuration arguments are as follows:
  279. *
  280. * - SaveAs: (string) The path to a file on disk to save the decrypted
  281. * object data. This will be handled by file_put_contents instead of the
  282. * Guzzle sink.
  283. *
  284. * - @MetadataStrategy: (MetadataStrategy|string|null) Strategy for reading
  285. * MetadataEnvelope information. Defaults to determining based on object
  286. * response headers. Can either be a class implementing MetadataStrategy,
  287. * a class name of a predefined strategy, or empty/null to default.
  288. * - @InstructionFileSuffix: (string) Suffix used when looking for an
  289. * instruction file if an InstructionFileMetadataHandler is being used.
  290. * - @CipherOptions: (array) Cipher options for decrypting data. A Cipher
  291. * is required. Accepts the following options:
  292. * - Aad: (string) Additional authentication data. This option is
  293. * passed directly to OpenSSL when using gcm. It is ignored when
  294. * using cbc.
  295. * - @KmsAllowDecryptWithAnyCmk: (bool) This allows decryption with
  296. * KMS materials for any KMS key ID, instead of needing the KMS key ID to
  297. * be specified and provided to the decrypt operation. Ignored for non-KMS
  298. * materials providers. Defaults to false.
  299. *
  300. * @return PromiseInterface
  301. *
  302. * @throws \InvalidArgumentException Thrown when required arguments are not
  303. * passed or are passed incorrectly.
  304. */
  305. public function getObjectAsync(array $args)
  306. {
  307. $provider = $this->getMaterialsProvider($args);
  308. unset($args['@MaterialsProvider']);
  309. $instructionFileSuffix = $this->getInstructionFileSuffix($args);
  310. unset($args['@InstructionFileSuffix']);
  311. $strategy = $this->getMetadataStrategy($args, $instructionFileSuffix);
  312. unset($args['@MetadataStrategy']);
  313. if (!isset($args['@SecurityProfile'])
  314. || !in_array($args['@SecurityProfile'], self::$supportedSecurityProfiles)
  315. ) {
  316. throw new CryptoException("@SecurityProfile is required and must be"
  317. . " set to 'V2' or 'V2_AND_LEGACY'");
  318. }
  319. // Only throw this legacy warning once per client
  320. if (in_array($args['@SecurityProfile'], self::$legacySecurityProfiles)
  321. && $this->legacyWarningCount < 1
  322. ) {
  323. $this->legacyWarningCount++;
  324. trigger_error(
  325. "This S3 Encryption Client operation is configured to"
  326. . " read encrypted data with legacy encryption modes. If you"
  327. . " don't have objects encrypted with these legacy modes,"
  328. . " you should disable support for them to enhance security. ",
  329. E_USER_WARNING
  330. );
  331. }
  332. $saveAs = null;
  333. if (!empty($args['SaveAs'])) {
  334. $saveAs = $args['SaveAs'];
  335. }
  336. $promise = $this->client->getObjectAsync($args)
  337. ->then(
  338. function ($result) use (
  339. $provider,
  340. $instructionFileSuffix,
  341. $strategy,
  342. $args
  343. ) {
  344. if ($strategy === null) {
  345. $strategy = $this->determineGetObjectStrategy(
  346. $result,
  347. $instructionFileSuffix
  348. );
  349. }
  350. $envelope = $strategy->load($args + [
  351. 'Metadata' => $result['Metadata']
  352. ]);
  353. $result['Body'] = $this->decrypt(
  354. $result['Body'],
  355. $provider,
  356. $envelope,
  357. $args
  358. );
  359. return $result;
  360. }
  361. )->then(
  362. function ($result) use ($saveAs) {
  363. if (!empty($saveAs)) {
  364. file_put_contents(
  365. $saveAs,
  366. (string)$result['Body'],
  367. LOCK_EX
  368. );
  369. }
  370. return $result;
  371. }
  372. );
  373. return $promise;
  374. }
  375. /**
  376. * Retrieves an object from S3 and decrypts the data in the 'Body' field.
  377. *
  378. * @param array $args Arguments for retrieving an object from S3 via
  379. * GetObject and decrypting it.
  380. *
  381. * The required configuration argument is as follows:
  382. *
  383. * - @MaterialsProvider: (MaterialsProviderInterface) Provides Cek, Iv, and Cek
  384. * encrypting/decrypting for decryption metadata. May have data loaded
  385. * from the MetadataEnvelope upon decryption.
  386. * - @SecurityProfile: (string) Must be set to 'V2' or 'V2_AND_LEGACY'.
  387. * - 'V2' indicates that only objects encrypted with S3EncryptionClientV2
  388. * content encryption and key wrap schemas are able to be decrypted.
  389. * - 'V2_AND_LEGACY' indicates that objects encrypted with both
  390. * S3EncryptionClientV2 and older legacy encryption clients are able
  391. * to be decrypted.
  392. *
  393. * The optional configuration arguments are as follows:
  394. *
  395. * - SaveAs: (string) The path to a file on disk to save the decrypted
  396. * object data. This will be handled by file_put_contents instead of the
  397. * Guzzle sink.
  398. * - @InstructionFileSuffix: (string|null) Suffix used when looking for an
  399. * instruction file if an InstructionFileMetadataHandler was detected.
  400. * - @CipherOptions: (array) Cipher options for encrypting data. A Cipher
  401. * is required. Accepts the following options:
  402. * - Aad: (string) Additional authentication data. This option is
  403. * passed directly to OpenSSL when using gcm. It is ignored when
  404. * using cbc.
  405. * - @KmsAllowDecryptWithAnyCmk: (bool) This allows decryption with
  406. * KMS materials for any KMS key ID, instead of needing the KMS key ID to
  407. * be specified and provided to the decrypt operation. Ignored for non-KMS
  408. * materials providers. Defaults to false.
  409. *
  410. * @return \Aws\Result GetObject call result with the 'Body' field
  411. * wrapped in a decryption stream with its metadata
  412. * information.
  413. *
  414. * @throws \InvalidArgumentException Thrown when arguments above are not
  415. * passed or are passed incorrectly.
  416. */
  417. public function getObject(array $args)
  418. {
  419. return $this->getObjectAsync($args)->wait();
  420. }
  421. }