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