1<?php
2namespace Aws\Crypto;
3
4use GuzzleHttp\Psr7;
5use GuzzleHttp\Psr7\LimitStream;
6use Psr\Http\Message\StreamInterface;
7
8trait DecryptionTrait
9{
10    /**
11     * Dependency to reverse lookup the openssl_* cipher name from the AESName
12     * in the MetadataEnvelope.
13     *
14     * @param $aesName
15     *
16     * @return string
17     *
18     * @internal
19     */
20    abstract protected function getCipherFromAesName($aesName);
21
22    /**
23     * Dependency to generate a CipherMethod from a set of inputs for loading
24     * in to an AesDecryptingStream.
25     *
26     * @param string $cipherName Name of the cipher to generate for decrypting.
27     * @param string $iv Base Initialization Vector for the cipher.
28     * @param int $keySize Size of the encryption key, in bits, that will be
29     *                     used.
30     *
31     * @return Cipher\CipherMethod
32     *
33     * @internal
34     */
35    abstract protected function buildCipherMethod($cipherName, $iv, $keySize);
36
37    /**
38     * Builds an AesStreamInterface using cipher options loaded from the
39     * MetadataEnvelope and MaterialsProvider. Can decrypt data from both the
40     * legacy and V2 encryption client workflows.
41     *
42     * @param string $cipherText Plain-text data to be encrypted using the
43     *                           materials, algorithm, and data provided.
44     * @param MaterialsProviderInterface $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 read from.
48     * @param array $cipherOptions Additional verification options.
49     *
50     * @return AesStreamInterface
51     *
52     * @throws \InvalidArgumentException Thrown when a value in $cipherOptions
53     *                                   is not valid.
54     *
55     * @internal
56     */
57    public function decrypt(
58        $cipherText,
59        MaterialsProviderInterface $provider,
60        MetadataEnvelope $envelope,
61        array $cipherOptions = []
62    ) {
63        $cipherOptions['Iv'] = base64_decode(
64            $envelope[MetadataEnvelope::IV_HEADER]
65        );
66
67        $cipherOptions['TagLength'] =
68            $envelope[MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER] / 8;
69
70        $cek = $provider->decryptCek(
71            base64_decode(
72                $envelope[MetadataEnvelope::CONTENT_KEY_V2_HEADER]
73            ),
74            json_decode(
75                $envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER],
76                true
77            )
78        );
79        $cipherOptions['KeySize'] = strlen($cek) * 8;
80        $cipherOptions['Cipher'] = $this->getCipherFromAesName(
81            $envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER]
82        );
83
84        $decryptionStream = $this->getDecryptingStream(
85            $cipherText,
86            $cek,
87            $cipherOptions
88        );
89        unset($cek);
90
91        return $decryptionStream;
92    }
93
94    private function getTagFromCiphertextStream(
95        StreamInterface $cipherText,
96        $tagLength
97    ) {
98        $cipherTextSize = $cipherText->getSize();
99        if ($cipherTextSize == null || $cipherTextSize <= 0) {
100            throw new \RuntimeException('Cannot decrypt a stream of unknown'
101                . ' size.');
102        }
103        return (string) new LimitStream(
104            $cipherText,
105            $tagLength,
106            $cipherTextSize - $tagLength
107        );
108    }
109
110    private function getStrippedCiphertextStream(
111        StreamInterface $cipherText,
112        $tagLength
113    ) {
114        $cipherTextSize = $cipherText->getSize();
115        if ($cipherTextSize == null || $cipherTextSize <= 0) {
116            throw new \RuntimeException('Cannot decrypt a stream of unknown'
117                . ' size.');
118        }
119        return new LimitStream(
120            $cipherText,
121            $cipherTextSize - $tagLength,
122            0
123        );
124    }
125
126    /**
127     * Generates a stream that wraps the cipher text with the proper cipher and
128     * uses the content encryption key (CEK) to decrypt the data when read.
129     *
130     * @param string $cipherText Plain-text data to be encrypted using the
131     *                           materials, algorithm, and data provided.
132     * @param string $cek A content encryption key for use by the stream for
133     *                    encrypting the plaintext data.
134     * @param array $cipherOptions Options for use in determining the cipher to
135     *                             be used for encrypting data.
136     *
137     * @return AesStreamInterface
138     *
139     * @internal
140     */
141    protected function getDecryptingStream(
142        $cipherText,
143        $cek,
144        $cipherOptions
145    ) {
146        $cipherTextStream = Psr7\Utils::streamFor($cipherText);
147        switch ($cipherOptions['Cipher']) {
148            case 'gcm':
149                $cipherOptions['Tag'] = $this->getTagFromCiphertextStream(
150                        $cipherTextStream,
151                        $cipherOptions['TagLength']
152                    );
153
154                return new AesGcmDecryptingStream(
155                    $this->getStrippedCiphertextStream(
156                        $cipherTextStream,
157                        $cipherOptions['TagLength']
158                    ),
159                    $cek,
160                    $cipherOptions['Iv'],
161                    $cipherOptions['Tag'],
162                    $cipherOptions['Aad'] = isset($cipherOptions['Aad'])
163                        ? $cipherOptions['Aad']
164                        : null,
165                    $cipherOptions['TagLength'] ?: null,
166                    $cipherOptions['KeySize']
167                );
168            default:
169                $cipherMethod = $this->buildCipherMethod(
170                    $cipherOptions['Cipher'],
171                    $cipherOptions['Iv'],
172                    $cipherOptions['KeySize']
173                );
174                return new AesDecryptingStream(
175                    $cipherTextStream,
176                    $cek,
177                    $cipherMethod
178                );
179        }
180    }
181}
182