<?php

namespace LowEntryUE4PHP\Classes\Internal;

use LowEntryUE4PHP\Classes\Internal\Rsa\RsaBigIntegerHelper;
use LowEntryUE4PHP\Classes\RsaKeys;
use LowEntryUE4PHP\Classes\RsaPrivateKey;
use LowEntryUE4PHP\Classes\RsaPublicKey;
use LowEntryUE4PHP\LowEntry;


class EncryptionRsa
{
	/**
	 * @param int $bits
	 *
	 * @return RsaKeys|null
	 */
	public static function generateKeys($bits)
	{
		for($i = 1; $i <= 100; $i++)
		{
			$p = RsaBigIntegerHelper::probablePrime((int) ($bits / 2));
			if($p == null)
			{
				return null;
			}
			
			$q = RsaBigIntegerHelper::probablePrime((int) ($bits / 2));
			if($q == null)
			{
				return null;
			}
			
			$n = $p->multiply($q);
			if($n == null)
			{
				return null;
			}
			
			$pm1 = $p->subtract(RsaBigIntegerHelper::ONE());
			if($pm1 == null)
			{
				return null;
			}
			
			$qm1 = $q->subtract(RsaBigIntegerHelper::ONE());
			if($qm1 == null)
			{
				return null;
			}
			
			$m = $pm1->multiply($qm1);
			if($m == null)
			{
				return null;
			}
			
			$e = RsaBigIntegerHelper::probablePrime((int) ($bits / 4));
			if($e == null)
			{
				return null;
			}
			
			while(true)
			{
				$mgcde = $m->gcd($e);
				if($mgcde == null)
				{
					return null;
				}
				
				if(RsaBigIntegerHelper::compareTo($mgcde, RsaBigIntegerHelper::ONE()) <= 0)
				{
					break;
				}
				if(RsaBigIntegerHelper::compareTo($e, $m) >= 0)
				{
					break;
				}
				
				$e = $e->add(RsaBigIntegerHelper::ONE());
				if($e == null)
				{
					return null;
				}
			}
			
			$d = $e->modInverse($m);
			if($d == null)
			{
				return null;
			}
			if($d->equals(RsaBigIntegerHelper::ZERO()))
			{
				continue;
			}
			
			$publicKey = new RsaPublicKey($n, $e);
			$privateKey = new RsaPrivateKey($n, $d, $p, $q);
			
			if(($privateKey->c2 == null) || $privateKey->dp->equals(RsaBigIntegerHelper::ZERO()) || $privateKey->dq->equals(RsaBigIntegerHelper::ZERO()))
			{
				continue;
			}
			return new RsaKeys($publicKey, $privateKey);
		}
		return null;
	}
	
	
	/**
	 * @param int[]        $bytes Byte array
	 * @param RsaPublicKey $publicKey
	 *
	 * @return int[] Byte array
	 */
	public static function encrypt($bytes, $publicKey)
	{
		if(($bytes == null) || ($publicKey == null) || ($publicKey->e == null) || ($publicKey->n == null))
		{
			return [];
		}
		$padded = PaddingOaep::pad($bytes, (int) \ceil(RsaBigIntegerHelper::bitLength($publicKey->n) / 8.0));
		$integer = RsaBigIntegerHelper::fromByteArray($padded);
		$integer = $integer->modPow($publicKey->e, $publicKey->n);
		if($integer == null)
		{
			return [];
		}
		return RsaBigIntegerHelper::toByteArray($integer);
	}
	
	/**
	 * @param int[]         $bytes Byte array
	 * @param RsaPrivateKey $privateKey
	 *
	 * @return int[] Byte array
	 */
	public static function decrypt($bytes, $privateKey)
	{
		if(($bytes == null) || ($privateKey == null) || ($privateKey->d == null) || ($privateKey->n == null))
		{
			return [];
		}
		$integer = RsaBigIntegerHelper::fromByteArray($bytes);
		if(($privateKey->c2 == null) || $privateKey->dp->equals(RsaBigIntegerHelper::ZERO()) || $privateKey->dq->equals(RsaBigIntegerHelper::ZERO()))
		{
			$integer = $integer->modPow($privateKey->d, $privateKey->n);
		}
		else
		{
			$cdp = $integer->modPow($privateKey->dp, $privateKey->p);
			if($cdp == null)
			{
				return [];
			}
			$cdq = $integer->modPow($privateKey->dq, $privateKey->q);
			if($cdq == null)
			{
				return [];
			}
			$u = $cdq->subtract($cdp);
			if($u == null)
			{
				return [];
			}
			$u = $u->multiply($privateKey->c2);
			if($u == null)
			{
				return [];
			}
			$u = RsaBigIntegerHelper::remainder($u, $privateKey->q);
			if($u == null)
			{
				return [];
			}
			if(RsaBigIntegerHelper::compareTo($u, RsaBigIntegerHelper::ZERO()) < 0)
			{
				$u = $u->add($privateKey->q);
				if($u == null)
				{
					return [];
				}
			}
			$integer = $u->multiply($privateKey->p);
			if($integer == null)
			{
				return [];
			}
			$integer = $cdp->add($integer);
		}
		if($integer == null)
		{
			return [];
		}
		$padded = RsaBigIntegerHelper::toByteArray($integer);
		return PaddingOaep::unpad($padded);
	}
	
	/**
	 * @param int[]         $hash Byte array
	 * @param RsaPrivateKey $privateKey
	 *
	 * @return int[] Byte array
	 */
	public static function sign($hash, $privateKey)
	{
		if(($hash == null) || ($privateKey == null) || ($privateKey->d == null) || ($privateKey->n == null))
		{
			return [];
		}
		$lengthN = (RsaBigIntegerHelper::bitLength($privateKey->n) + 7) / 8;
		$randomBytesCount = $lengthN - (\count($hash) + 3);
		if($randomBytesCount < ($lengthN / 2))
		{
			// the given hash was too large for the given private key
			return [];
		}
		$randomBytes = LowEntry::createArray($randomBytesCount, 0);
		self::randomSecure($randomBytes);
		$paddedHash = self::mergeBytes([0, 2], $randomBytes, [0], $hash);
		$signature = RsaBigIntegerHelper::fromByteArray($paddedHash)->modPow($privateKey->d, $privateKey->n);
		if($signature == null)
		{
			return [];
		}
		return RsaBigIntegerHelper::toByteArray($signature);
	}
	
	/**
	 * @param int[]        $signature    Byte array
	 * @param int[]        $expectedHash Byte array
	 * @param RsaPublicKey $publicKey
	 *
	 * @return bool
	 */
	public static function verifySignature($signature, $expectedHash, $publicKey)
	{
		if(($signature == null) || ($expectedHash == null) || ($publicKey == null) || ($publicKey->e == null) || ($publicKey->n == null))
		{
			return false;
		}
		$signaturePaddedHash = RsaBigIntegerHelper::fromByteArray($signature)->modPow($publicKey->e, $publicKey->n);
		if($signaturePaddedHash == null)
		{
			return false;
		}
		$signaturePaddedHashBytes = RsaBigIntegerHelper::toByteArray($signaturePaddedHash);
		$signatureHash = self::bytesSubArray($signaturePaddedHashBytes, \count($signaturePaddedHashBytes) - \count($expectedHash), \count($expectedHash));
		return self::areBytesEqual($signatureHash, $expectedHash);
	}
	
	
	/**
	 * @param RsaPublicKey $publicKey
	 *
	 * @return int[] Byte array
	 */
	public static function publicKeyToBytes($publicKey)
	{
		if(($publicKey == null) || ($publicKey->e == null) || ($publicKey->n == null))
		{
			return [];
		}
		$n = RsaBigIntegerHelper::toByteArray($publicKey->n);
		$e = RsaBigIntegerHelper::toByteArray($publicKey->e);
		return self::mergeBytes(self::uintegerToBytes(\count($n)), $n, $e);
	}
	
	/**
	 * @param int[] $bytes Byte array
	 *
	 * @return RsaPublicKey|null
	 */
	public static function bytesToPublicKey($bytes)
	{
		if($bytes == null)
		{
			return null;
		}
		$n = self::bytesToUinteger($bytes);
		if($n <= 0)
		{
			return null;
		}
		$sn = (($n <= 127) ? 1 : 4);
		$e = \count($bytes) - ($sn + $n);
		return new RsaPublicKey(RsaBigIntegerHelper::fromByteArray(self::bytesSubArray($bytes, $sn, $n)), RsaBigIntegerHelper::fromByteArray(self::bytesSubArray($bytes, $sn + $n, $e)));
	}
	
	
	/**
	 * @param RsaPrivateKey $privateKey
	 *
	 * @return int[] Byte array
	 */
	public static function privateKeyToBytes($privateKey)
	{
		if(($privateKey == null) || ($privateKey->d == null) || ($privateKey->n == null) || ($privateKey->p == null) || ($privateKey->q == null))
		{
			return [];
		}
		$n = RsaBigIntegerHelper::toByteArray($privateKey->n);
		$d = RsaBigIntegerHelper::toByteArray($privateKey->d);
		$p = RsaBigIntegerHelper::toByteArray($privateKey->p);
		$q = RsaBigIntegerHelper::toByteArray($privateKey->q);
		return self::mergeBytes(self::uintegerToBytes(\count($n)), $n, self::uintegerToBytes(\count($d)), $d, self::uintegerToBytes(\count($p)), $p, $q);
	}
	
	/**
	 * @param int[] $bytes Byte array
	 *
	 * @return RsaPrivateKey|null
	 */
	public static function bytesToPrivateKey($bytes)
	{
		if($bytes == null)
		{
			return null;
		}
		
		$n = self::bytesToUinteger($bytes);
		if($n <= 0)
		{
			return null;
		}
		$sn = (($n <= 127) ? 1 : 4);
		
		$d = self::bytesToUinteger($bytes, $sn + $n);
		if($d <= 0)
		{
			return null;
		}
		$sd = (($d <= 127) ? 1 : 4);
		
		$p = self::bytesToUinteger($bytes, $sn + $n + $sd + $d);
		if($p <= 0)
		{
			return null;
		}
		$sp = (($p <= 127) ? 1 : 4);
		
		$q = \count($bytes) - ($sn + $n + $sd + $d + $sp + $p);
		
		return new RsaPrivateKey(RsaBigIntegerHelper::fromByteArray(self::bytesSubArray($bytes, $sn, $n)), RsaBigIntegerHelper::fromByteArray(self::bytesSubArray($bytes, $sn + $n + $sd, $d)), RsaBigIntegerHelper::fromByteArray(self::bytesSubArray($bytes, $sn + $n + $sd + $d + $sp, $p)), RsaBigIntegerHelper::fromByteArray(self::bytesSubArray($bytes, $sn + $n + $sd + $d + $sp + $p, $q)));
	}
	
	
	/**
	 * @param int $value
	 *
	 * @return int[] Byte array
	 */
	private static function uintegerToBytes($value)
	{
		if($value <= 127)
		{
			return [LowEntry::castToByte($value)];
		}
		else
		{
			return [LowEntry::castToByte(($value >> 24) | (1 << 7)), LowEntry::castToByte($value >> 16), LowEntry::castToByte($value >> 8), LowEntry::castToByte($value)];
		}
	}
	
	/**
	 * @param int[]    $bytes Byte array
	 * @param int|null $offset
	 *
	 * @return int
	 */
	private static function bytesToUinteger($bytes, $offset = null)
	{
		$bytesLength = \count($bytes);
		if($bytesLength <= 0)
		{
			return 0;
		}
		if($offset === null)
		{
			$b = $bytes[0];
			if((($b >> 7) & 1) == 0)
			{
				return ($b & 0xFF);
			}
			
			if($bytesLength <= 3)
			{
				return 0;
			}
			$value = ((($b & 0xFF) & ~(1 << 7)) << 24) | (($bytes[1] & 0xFF) << 16) | (($bytes[2] & 0xFF) << 8) | ($bytes[3] & 0xFF);
			if($value <= 127)
			{
				return 0;
			}
			return $value;
		}
		else
		{
			$b = $bytes[$offset];
			if((($b >> 7) & 1) == 0)
			{
				return ($b & 0xFF);
			}
			
			if($bytesLength <= ($offset + 3))
			{
				return 0;
			}
			$value = ((($b & 0xFF) & ~(1 << 7)) << 24) | (($bytes[$offset + 1] & 0xFF) << 16) | (($bytes[$offset + 2] & 0xFF) << 8) | ($bytes[$offset + 3] & 0xFF);
			if($value <= 127)
			{
				return 0;
			}
			return $value;
		}
	}
	
	/**
	 * @param int[] $bytes Byte array
	 * @param int   $index
	 * @param int   $length
	 *
	 * @return int[] Byte array
	 */
	private static function bytesSubArray($bytes, $index, $length)
	{
		if(empty($bytes))
		{
			return [];
		}
		$bytesLength = \count($bytes);
		
		if($index < 0)
		{
			$length += $index;
			$index = 0;
		}
		if($length > ($bytesLength - $index))
		{
			$length = $bytesLength - $index;
		}
		if($length <= 0)
		{
			return [];
		}
		
		if(($index == 0) && ($length == $bytesLength))
		{
			return $bytes;
		}
		$c = LowEntry::createArray($length, 0);
		LowEntry::systemArrayCopy($bytes, $index, $c, 0, \count($c));
		return $c;
	}
	
	/**
	 * @param int[] ...$arrays Byte arrays
	 *
	 * @return int[] Byte array
	 */
	private static function mergeBytes(...$arrays)
	{
		if(empty($arrays))
		{
			return [];
		}
		if(\count($arrays) == 1)
		{
			$array = $arrays[0];
			if($array == null)
			{
				return [];
			}
			return $array;
		}
		$length = 0;
		foreach($arrays as $array)
		{
			if($array != null)
			{
				$length += \count($array);
			}
		}
		if($length <= 0)
		{
			return [];
		}
		$merged = LowEntry::createArray($length, 0);
		$index = 0;
		foreach($arrays as $array)
		{
			if($array != null)
			{
				LowEntry::systemArrayCopy($array, 0, $merged, $index, \count($array));
				$index += \count($array);
			}
		}
		return $merged;
	}
	
	/**
	 * @param int[] $a Byte array
	 * @param int[] $b Byte array
	 *
	 * @return bool
	 */
	public static function areBytesEqual($a, $b)
	{
		if(!isset($a) || !isset($b))
		{
			return false;
		}
		if(\count($a) != \count($b))
		{
			return false;
		}
		
		$aLength = \count($a);
		for($i = 0; $i < $aLength; $i++)
		{
			if($a[$i] != $b[$i])
			{
				return false;
			}
		}
		return true;
	}
	
	/**
	 * @param int[]|null $bytes Byte array
	 *
	 * @return int
	 */
	private static function random(&$bytes = null)
	{
		if($bytes === null)
		{
			return LowEntry::castToByte(\mt_rand());
		}
		
		$length = \count($bytes);
		for($i = 0; $i < $length; $i++)
		{
			$bytes[$i] = LowEntry::castToByte(\mt_rand());
		}
		return 0;
	}
	
	/**
	 * @param int[] $bytes Byte array
	 */
	private static function randomSecure(&$bytes)
	{
		$length = \count($bytes);
		
		$newBytes = false;
		if(!$newBytes && \function_exists('random_bytes'))
		{
			try
			{
				/** @noinspection UnknownInspectionInspection,RedundantSuppression,PhpElementIsNotAvailableInCurrentPhpVersionInspection */
				$newBytes = \random_bytes($length);
			}
			catch(\Exception $e)
			{
				$newBytes = false;
			}
		}
		if(!$newBytes && \function_exists('openssl_random_pseudo_bytes'))
		{
			try
			{
				/** @noinspection PhpComposerExtensionStubsInspection, CryptographicallySecureRandomnessInspection */
				$newBytes = \openssl_random_pseudo_bytes($length);
			}
			catch(\Exception $e)
			{
				$newBytes = false;
			}
		}
		if($newBytes === false)
		{
			self::random($bytes);
			return;
		}
		$newBytes = LowEntry::stringToBytesUtf8($newBytes);
		
		for($i = 0; $i < $length; $i++)
		{
			$bytes[$i] = $newBytes[$i];
		}
	}
}
