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