<?php namespace Intellex\Upload;

/**
 * Used for simple encrypting and decrypting.
 */
class Crypto {

	/** @var string The method to use for encryption. */
	const METHOD = 'aes-256-ctr';

	/** @var string The method to use to verify. */
	const HASH = 'sha256';

	/**
	 * Encrypts (but does not authenticate) a message
	 *
	 * @param string $message The message to encrypt.
	 * @param string $key     Encryption key, as raw binary.
	 *
	 * @return string Resulted crypted message as raw binary.
	 */
	private static function _encrypt($message, $key) {

		// Init nonce
		$nonceSize = openssl_cipher_iv_length(static::METHOD);
		$nonce = openssl_random_pseudo_bytes($nonceSize);

		// Cipher the text
		$ciphertext = openssl_encrypt(
			$message,
			static::METHOD,
			$key,
			OPENSSL_RAW_DATA,
			$nonce
		);

		return $nonce . $ciphertext;
	}

	/**
	 * Decrypts (but does not verify) a message
	 *
	 * @param string $message The message to encrypt.
	 * @param string $key     Encryption key, as raw binary.
	 *
	 * @return string The original input.
	 */
	private static function _decrypt($message, $key) {

		// Init decrypt
		$nonceSize = openssl_cipher_iv_length(static::METHOD);
		$nonce = mb_substr($message, 0, $nonceSize, '8bit');
		$ciphertext = mb_substr($message, $nonceSize, null, '8bit');

		// Decrypt
		$plaintext = openssl_decrypt(
			$ciphertext,
			static::METHOD,
			$key,
			OPENSSL_RAW_DATA,
			$nonce
		);

		return $plaintext;
	}

	/**
	 * Encrypts and MACs an input.
	 *
	 * @param string $message The message to encrypt.
	 * @param string $key     Encryption key, as raw binary.
	 *
	 * @return string A crypted message.
	 */
	public static function encrypt($message, $key) {
		list($encKey, $authKey) = static::splitKeys($key);

		// Pass to unsafe encryption
		$ciphertext = static::_encrypt($message, $encKey);

		// Calculate a MAC of the IV and ciphertext
		$mac = hash_hmac(static::HASH, $ciphertext, $authKey, true);

		// Prepend MAC to the ciphertext and return to caller
		return $mac . $ciphertext;
	}

	/**
	 * Decrypts a message (after verifying integrity)
	 *
	 * @param string $message The encrypted raw binary.
	 * @param string $key     Encryption key, as raw binary.
	 *
	 * @return string The original input.
	 * @throws \Exception On signature failed.
	 */
	public static function decrypt($message, $key) {
		list($encKey, $authKey) = static::splitKeys($key);

		// Hash Size, in case HASH has changed
		$hs = mb_strlen(hash(static::HASH, '', true), '8bit');
		$mac = mb_substr($message, 0, $hs, '8bit');
		$ciphertext = mb_substr($message, $hs, null, '8bit');

		$calculated = hash_hmac(
			static::HASH,
			$ciphertext,
			$authKey,
			true
		);

		// Validate signature
		if (!static::hashEquals($mac, $calculated)) {
			throw new \Exception('Signature verification failed.');
		}

		return static::_decrypt($ciphertext, $encKey);
	}

	/**
	 * Splits a key into two separate keys; one for encryption and the other for authentication
	 *
	 * @param string $masterKey The master encryption key, as raw binary.
	 *
	 * @return array Two keys to use for encryption and authentication.
	 */
	private static function splitKeys($masterKey) {
		return [
			hash_hmac(static::HASH, 'ENCRYPTION', $masterKey, true),
			hash_hmac(static::HASH, 'AUTHENTICATION', $masterKey, true)
		];
	}

	/**
	 * Compare two strings without leaking timing information
	 * @ref https://paragonie.com/b/WS1DLx6BnpsdaVQW
	 *
	 * @param string $a The first string to compare.
	 * @param string $b The second string to compare.
	 *
	 * @return boolean True if both string are equal, false otherwise.
	 */
	private static function hashEquals($a, $b) {
		if (function_exists('hash_equals')) {
			return hash_equals($a, $b);
		}

		$nonce = openssl_random_pseudo_bytes(32);
		return hash_hmac(static::HASH, $a, $nonce) === hash_hmac(static::HASH, $b, $nonce);
	}

}
