1<?php 2namespace Aws\Crypto; 3 4use GuzzleHttp\Psr7; 5use GuzzleHttp\Psr7\AppendStream; 6use GuzzleHttp\Psr7\Stream; 7use Psr\Http\Message\StreamInterface; 8 9trait EncryptionTraitV2 10{ 11 private static $allowedOptions = [ 12 'Cipher' => true, 13 'KeySize' => true, 14 'Aad' => true, 15 ]; 16 17 private static $encryptClasses = [ 18 'gcm' => AesGcmEncryptingStream::class 19 ]; 20 21 /** 22 * Dependency to generate a CipherMethod from a set of inputs for loading 23 * in to an AesEncryptingStream. 24 * 25 * @param string $cipherName Name of the cipher to generate for encrypting. 26 * @param string $iv Base Initialization Vector for the cipher. 27 * @param int $keySize Size of the encryption key, in bits, that will be 28 * used. 29 * 30 * @return Cipher\CipherMethod 31 * 32 * @internal 33 */ 34 abstract protected function buildCipherMethod($cipherName, $iv, $keySize); 35 36 /** 37 * Builds an AesStreamInterface and populates encryption metadata into the 38 * supplied envelope. 39 * 40 * @param Stream $plaintext Plain-text data to be encrypted using the 41 * materials, algorithm, and data provided. 42 * @param array $options Options for use in encryption, including cipher 43 * options, and encryption context. 44 * @param MaterialsProviderV2 $provider A provider to supply and encrypt 45 * materials used in encryption. 46 * @param MetadataEnvelope $envelope A storage envelope for encryption 47 * metadata to be added to. 48 * 49 * @return StreamInterface 50 * 51 * @throws \InvalidArgumentException Thrown when a value in $options['@CipherOptions'] 52 * is not valid. 53 *s 54 * @internal 55 */ 56 public function encrypt( 57 Stream $plaintext, 58 array $options, 59 MaterialsProviderV2 $provider, 60 MetadataEnvelope $envelope 61 ) { 62 $options = array_change_key_case($options); 63 $cipherOptions = array_intersect_key( 64 $options['@cipheroptions'], 65 self::$allowedOptions 66 ); 67 68 if (empty($cipherOptions['Cipher'])) { 69 throw new \InvalidArgumentException('An encryption cipher must be' 70 . ' specified in @CipherOptions["Cipher"].'); 71 } 72 73 $cipherOptions['Cipher'] = strtolower($cipherOptions['Cipher']); 74 75 if (!self::isSupportedCipher($cipherOptions['Cipher'])) { 76 throw new \InvalidArgumentException('The cipher requested is not' 77 . ' supported by the SDK.'); 78 } 79 80 if (empty($cipherOptions['KeySize'])) { 81 $cipherOptions['KeySize'] = 256; 82 } 83 if (!is_int($cipherOptions['KeySize'])) { 84 throw new \InvalidArgumentException('The cipher "KeySize" must be' 85 . ' an integer.'); 86 } 87 88 if (!MaterialsProviderV2::isSupportedKeySize( 89 $cipherOptions['KeySize'] 90 )) { 91 throw new \InvalidArgumentException('The cipher "KeySize" requested' 92 . ' is not supported by AES (128 or 256).'); 93 } 94 95 $cipherOptions['Iv'] = $provider->generateIv( 96 $this->getCipherOpenSslName( 97 $cipherOptions['Cipher'], 98 $cipherOptions['KeySize'] 99 ) 100 ); 101 102 $encryptClass = self::$encryptClasses[$cipherOptions['Cipher']]; 103 $aesName = $encryptClass::getStaticAesName(); 104 $materialsDescription = ['aws:x-amz-cek-alg' => $aesName]; 105 106 $keys = $provider->generateCek( 107 $cipherOptions['KeySize'], 108 $materialsDescription, 109 $options 110 ); 111 112 // Some providers modify materials description based on options 113 if (isset($keys['UpdatedContext'])) { 114 $materialsDescription = $keys['UpdatedContext']; 115 } 116 117 $encryptingStream = $this->getEncryptingStream( 118 $plaintext, 119 $keys['Plaintext'], 120 $cipherOptions 121 ); 122 123 // Populate envelope data 124 $envelope[MetadataEnvelope::CONTENT_KEY_V2_HEADER] = $keys['Ciphertext']; 125 unset($keys); 126 127 $envelope[MetadataEnvelope::IV_HEADER] = 128 base64_encode($cipherOptions['Iv']); 129 $envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER] = 130 $provider->getWrapAlgorithmName(); 131 $envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER] = $aesName; 132 $envelope[MetadataEnvelope::UNENCRYPTED_CONTENT_LENGTH_HEADER] = 133 strlen($plaintext); 134 $envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER] = 135 json_encode($materialsDescription); 136 if (!empty($cipherOptions['Tag'])) { 137 $envelope[MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER] = 138 strlen($cipherOptions['Tag']) * 8; 139 } 140 141 return $encryptingStream; 142 } 143 144 /** 145 * Generates a stream that wraps the plaintext with the proper cipher and 146 * uses the content encryption key (CEK) to encrypt the data when read. 147 * 148 * @param Stream $plaintext Plain-text data to be encrypted using the 149 * materials, algorithm, and data provided. 150 * @param string $cek A content encryption key for use by the stream for 151 * encrypting the plaintext data. 152 * @param array $cipherOptions Options for use in determining the cipher to 153 * be used for encrypting data. 154 * 155 * @return [AesStreamInterface, string] 156 * 157 * @internal 158 */ 159 protected function getEncryptingStream( 160 Stream $plaintext, 161 $cek, 162 &$cipherOptions 163 ) { 164 switch ($cipherOptions['Cipher']) { 165 // Only 'gcm' is supported for encryption currently 166 case 'gcm': 167 $cipherOptions['TagLength'] = 16; 168 $encryptClass = self::$encryptClasses['gcm']; 169 $cipherTextStream = new $encryptClass( 170 $plaintext, 171 $cek, 172 $cipherOptions['Iv'], 173 $cipherOptions['Aad'] = isset($cipherOptions['Aad']) 174 ? $cipherOptions['Aad'] 175 : '', 176 $cipherOptions['TagLength'], 177 $cipherOptions['KeySize'] 178 ); 179 180 if (!empty($cipherOptions['Aad'])) { 181 trigger_error("'Aad' has been supplied for content encryption" 182 . " with " . $cipherTextStream->getAesName() . ". The" 183 . " PHP SDK encryption client can decrypt an object" 184 . " encrypted in this way, but other AWS SDKs may not be" 185 . " able to.", E_USER_WARNING); 186 } 187 188 $appendStream = new AppendStream([ 189 $cipherTextStream->createStream() 190 ]); 191 $cipherOptions['Tag'] = $cipherTextStream->getTag(); 192 $appendStream->addStream(Psr7\Utils::streamFor($cipherOptions['Tag'])); 193 return $appendStream; 194 } 195 } 196} 197