<?php

namespace Aws\Crypto;

use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\LimitStream;
use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
use RuntimeException;

trait DecryptionTrait {
	/**
	 * Builds an AesStreamInterface using cipher options loaded from the
	 * MetadataEnvelope and MaterialsProvider.
	 *
	 * @param string $cipherText Plain-text data to be encrypted using the
	 *                           materials, algorithm, and data provided.
	 * @param MaterialsProvider $provider A provider to supply and encrypt
	 *                                    materials used in encryption.
	 * @param MetadataEnvelope $envelope A storage envelope for encryption
	 *                                   metadata to be read from.
	 * @param array $cipherOptions Additional verification options.
	 *
	 * @return AesStreamInterface
	 *
	 * @throws InvalidArgumentException Thrown when a value in $cipherOptions
	 *                                   is not valid.
	 *
	 * @internal
	 */
	protected function decrypt(
		$cipherText,
		MaterialsProvider $provider,
		MetadataEnvelope $envelope,
		array $cipherOptions = []
	) {
		$cipherOptions['Iv'] = base64_decode(
			$envelope[ MetadataEnvelope::IV_HEADER ]
		);

		$cipherOptions['TagLength'] =
			$envelope[ MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER ] / 8;

		$cek                      = $provider->decryptCek(
			base64_decode(
				$envelope[ MetadataEnvelope::CONTENT_KEY_V2_HEADER ]
			),
			json_decode(
				$envelope[ MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER ],
				true
			)
		);
		$cipherOptions['KeySize'] = strlen( $cek ) * 8;
		$cipherOptions['Cipher']  = $this->getCipherFromAesName(
			$envelope[ MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER ]
		);

		$decryptionSteam = $this->getDecryptingStream(
			$cipherText,
			$cek,
			$cipherOptions
		);
		unset( $cek );

		return $decryptionSteam;
	}

	/**
	 * Dependency to reverse lookup the openssl_* cipher name from the AESName
	 * in the MetadataEnvelope.
	 *
	 * @param $aesName
	 *
	 * @return string
	 *
	 * @internal
	 */
	abstract protected function getCipherFromAesName( $aesName );

	/**
	 * Generates a stream that wraps the cipher text with the proper cipher and
	 * uses the content encryption key (CEK) to decrypt the data when read.
	 *
	 * @param string $cipherText Plain-text data to be encrypted using the
	 *                           materials, algorithm, and data provided.
	 * @param string $cek A content encryption key for use by the stream for
	 *                    encrypting the plaintext data.
	 * @param array $cipherOptions Options for use in determining the cipher to
	 *                             be used for encrypting data.
	 *
	 * @return AesStreamInterface
	 *
	 * @internal
	 */
	protected function getDecryptingStream(
		$cipherText,
		$cek,
		$cipherOptions
	) {
		$cipherTextStream = Psr7\stream_for( $cipherText );
		switch ( $cipherOptions['Cipher'] ) {
			case 'gcm':
				$cipherOptions['Tag'] = $this->getTagFromCiphertextStream(
					$cipherTextStream,
					$cipherOptions['TagLength']
				);

				return new AesGcmDecryptingStream(
					$this->getStrippedCiphertextStream(
						$cipherTextStream,
						$cipherOptions['TagLength']
					),
					$cek,
					$cipherOptions['Iv'],
					$cipherOptions['Tag'],
					$cipherOptions['Aad'] = isset( $cipherOptions['Aad'] )
						? $cipherOptions['Aad']
						: null,
					$cipherOptions['TagLength'] ?: null,
					$cipherOptions['KeySize']
				);
			default:
				$cipherMethod = $this->buildCipherMethod(
					$cipherOptions['Cipher'],
					$cipherOptions['Iv'],
					$cipherOptions['KeySize']
				);

				return new AesDecryptingStream(
					$cipherTextStream,
					$cek,
					$cipherMethod
				);
		}
	}

	private function getTagFromCiphertextStream(
		StreamInterface $cipherText,
		$tagLength
	) {
		$cipherTextSize = $cipherText->getSize();
		if ( $cipherTextSize == null || $cipherTextSize <= 0 ) {
			throw new RuntimeException( 'Cannot decrypt a stream of unknown'
			                             . ' size.' );
		}

		return (string) new LimitStream(
			$cipherText,
			$tagLength,
			$cipherTextSize - $tagLength
		);
	}

	private function getStrippedCiphertextStream(
		StreamInterface $cipherText,
		$tagLength
	) {
		$cipherTextSize = $cipherText->getSize();
		if ( $cipherTextSize == null || $cipherTextSize <= 0 ) {
			throw new RuntimeException( 'Cannot decrypt a stream of unknown'
			                             . ' size.' );
		}

		return new LimitStream(
			$cipherText,
			$cipherTextSize - $tagLength,
			0
		);
	}

	/**
	 * Dependency to generate a CipherMethod from a set of inputs for loading
	 * in to an AesDecryptingStream.
	 *
	 * @param string $cipherName Name of the cipher to generate for decrypting.
	 * @param string $iv Base Initialization Vector for the cipher.
	 * @param int $keySize Size of the encryption key, in bits, that will be
	 *                     used.
	 *
	 * @return Cipher\CipherMethod
	 *
	 * @internal
	 */
	abstract protected function buildCipherMethod( $cipherName, $iv, $keySize );
}
