<?php

	include './SecretContext.php';
	include './MagicCrypt.php';

	class SecurityUtil
	{

		private $BASE64_ARRAY = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
		private $SEPARATOR_CHAR_MAP;

		function __construct()
		{
			if(!defined("PHONE_SEPARATOR_CHAR"))
			{
				define('PHONE_SEPARATOR_CHAR','$');
			}
			if(!defined("NICK_SEPARATOR_CHAR"))
			{
				define('NICK_SEPARATOR_CHAR','~');
			}
			if(!defined("NORMAL_SEPARATOR_CHAR"))
			{
				define('NORMAL_SEPARATOR_CHAR',chr(1));
			}

			$this->SEPARATOR_CHAR_MAP['nick'] = NICK_SEPARATOR_CHAR;
			$this->SEPARATOR_CHAR_MAP['simple'] = NICK_SEPARATOR_CHAR;
			$this->SEPARATOR_CHAR_MAP['receiver_name'] = NICK_SEPARATOR_CHAR;
			$this->SEPARATOR_CHAR_MAP['search'] = NICK_SEPARATOR_CHAR;
			$this->SEPARATOR_CHAR_MAP['normal'] = NORMAL_SEPARATOR_CHAR;
			$this->SEPARATOR_CHAR_MAP['phone'] = PHONE_SEPARATOR_CHAR;

		}

		/*
		* 判断是否是base64格式的数据
		*/
		function isBase64Str($str)
		{
			$strLen = strlen($str);
			for($i = 0; $i < $strLen ; $i++)
			{
				if(!$this->isBase64Char($str[$i]))
				{
					return false;
				}
			}
			return true;
		}

		/*
		* 判断是否是base64格式的字符
		*/
		function isBase64Char($char)
		{
			return strpos($this->BASE64_ARRAY,$char) !== false;
		}

		/*
		* 使用sep字符进行trim
		*/
		function trimBySep($str,$sep)
		{
			$start = 0;
			$end = strlen($str);
			for($i = 0; $i < $end; $i++)
			{
				if($str[$i] == $sep)
				{
					$start = $i + 1;
				}
				else
				{
					break;
				}
			}
			for($i = $end -1 ; $i >= 0; $i--)
			{
				if($str[$i] == $sep)
				{
					$end = $i - 1;
				}
				else
				{
					break;
				}
			}
			return substr($str,$start,$end);
		}

		function checkEncryptData($dataArray)
		{
			if(count($dataArray) == 2){
				return  $this->isBase64Str($dataArray[0]);
			}else{
				return  $this->isBase64Str($dataArray[0]) && $this->isBase64Str($dataArray[1]);
			}
		}

		/*
		* 判断是否是加密数据
		*/
		function isEncryptDataArray($array,$type)
		{
			foreach ($array as $value) {
				if(!$this->isEncryptData($value,$type)){
					return false;
				}
			}
			return true;
		}

		/**
		* 判断是否是已加密的数据，数据必须是同一个类型
		*/
		function isPartEncryptData($array,$type)
		{
			$result = false;
			foreach ($array as $value) {
				if($this->isEncryptData($value,$type)){
					$result = true;
					break;
				}
			}
			return $result;
		}

		/*
		* 判断是否是加密数据
		*/
		function isEncryptData($data,$type)
		{
			if(!is_string($data) || strlen($data) < 4)
			{
				return false;
			}

			$separator = $this->SEPARATOR_CHAR_MAP[$type];
			$strlen = strlen($data);
			if($data[0] != $separator || $data[$strlen -1] != $separator)
			{
				return false;
			}

			$dataArray = explode($separator,$this->trimBySep($data,$separator));
			$arrayLength = count($dataArray);

			if($separator == PHONE_SEPARATOR_CHAR)
			{
				if($arrayLength != 3)
				{
					return false;
				}
				if($data[$strlen - 2] == $separator)
				{
					return $this->checkEncryptData($dataArray);
				} 
				else
				{
					$version = $dataArray[$arrayLength -1];
					if(is_numeric($version))
					{
						$base64Val = $dataArray[$arrayLength -2];
						return $this->isBase64Str($base64Val);
					}
				}
			}else{
				if($data[strlen($data) - 2] == $separator && $arrayLength == 3)
				{
					return $this->checkEncryptData($dataArray);
				}
				else if($arrayLength == 2)
				{
					return $this->checkEncryptData($dataArray);
				}
				else
				{
					return false;
				}
			}
		}

		function search($data, $type,$secretContext)
		{
			$separator = $this->SEPARATOR_CHAR_MAP[$type];
			if('phone' == $type) {
		        if (strlen($data) != 4 ) {
		            throw new Exception("phoneNumber error");
		        }
		        return $separator.$this->hmacMD5EncryptToBase64($data, $secretContext->secret).$separator;
			} else {
				$compressLen = $this->getArrayValue($secretContext->appConfig,'encrypt_index_compress_len',3);
				$slideSize = $this->getArrayValue($secretContext->appConfig,'encrypt_slide_size',4);

				$slideList = $this->getSlideWindows($data, $slideSize);
		        $builder = '';
		        foreach ($slideList as $slide) {
					$builder .= $this->hmacMD5EncryptToBase64($slide,$secretContext->secret,$compressLen);
				}
		        return $builder;
			}
		}

		/*
		* 加密逻辑
		*/
		function encrypt($data,$type,$version,$secretContext)
		{
			if(!is_string($data))
			{
				return false;
			}

			$separator = $this->SEPARATOR_CHAR_MAP[$type];
			$isIndexEncrypt = $this->isIndexEncrypt($type,$version,$secretContext);
			if($isIndexEncrypt || $type == "search"){
				if('phone' == $type) {
					return $this->encryptPhoneIndex($data,$separator,$secretContext);
				} else {
					$compressLen = $this->getArrayValue($secretContext->appConfig,'encrypt_index_compress_len',3);
					$slideSize = $this->getArrayValue($secretContext->appConfig,'encrypt_slide_size',4);
					return $this->encryptNormalIndex($data,$compressLen,$slideSize,$separator,$secretContext);
				}
			}else{
				if('phone' == $type) {
					return $this->encryptPhone($data,$separator,$secretContext);
				} else {
					return $this->encryptNormal($data,$separator,$secretContext);
				}
			}

		}

		/*
		* 加密逻辑,手机号码格式
		*/
		function encryptPhone($data,$separator,$secretContext)
		{
			$len = strlen($data);
			if($len < 11)
			{
				return $data;
			}
			$prefixNumber = substr($data,0,$len -8);
			$last8Number =  substr($data,$len -8,$len);

			return $separator.$prefixNumber.$separator.Security::encrypt($last8Number,$secretContext->secret)
				  .$separator.$secretContext->secretVersion.$separator ;
		}

		/*
		* 加密逻辑,非手机号码格式
		*/
		function encryptNormal($data,$separator,$secretContext)
		{
			return $separator.Security::encrypt($data,$secretContext->secret)
							 .$separator.$secretContext->secretVersion.$separator;
		}

		/*
		* 解密逻辑
		*/
		function decrypt($data,$type,$secretContext)
		{			
			if(!$this->isEncryptData($data,$type))
			{
				throw new Exception("数据[".$data."]不是类型为[".$type."]的加密数据");
			}
			$dataLen = strlen($data);
			$separator = $this->SEPARATOR_CHAR_MAP[$type];

			$secretData = null;
			if($data[$dataLen - 2] == $separator){
				$secretData = $this->getIndexSecretData($data,$separator);
			}else{
				$secretData = $this->getSecretData($data,$separator);
			}
			
			if($secretData == null){
				return $data;
			}

			$result = Security::decrypt($secretData->originalBase64Value,$secretContext->secret);

			if($separator == PHONE_SEPARATOR_CHAR && !$secretData->search)
			{
				return $secretData->originalValue.$result;
			}
			return $result;
		}

		/*
		* 判断是否是公钥数据
		*/
		function isPublicData($data,$type)
		{
			$secretData = $this->getSecretDataByType($data,$type);
			if(empty($secretData)){
				return false;
			}
			if(intval($secretData->secretVersion) < 0){
				return true;
			}
			return false;
		}

		function getSecretDataByType($data,$type)
		{
			$separator = $this->SEPARATOR_CHAR_MAP[$type];
			$dataLen = strlen($data);

			if($data[$dataLen - 2] == $separator){
				return $secretData = $this->getIndexSecretData($data,$separator);
			}else{
				return  $secretData = $this->getSecretData($data,$separator);
			}
		}

		/*
		* 分解密文
		*/
		function getSecretData($data,$separator)
		{
			$secretData = new SecretData;
			$dataArray = explode($separator,$this->trimBySep($data,$separator));
			$arrayLength = count($dataArray);

			if($separator == PHONE_SEPARATOR_CHAR)
			{
				if($arrayLength != 3){
					return null;
				}else{
					$version = $dataArray[2];
					if(is_numeric($version))
					{
						$secretData->originalValue = $dataArray[0];
						$secretData->originalBase64Value = $dataArray[1];
						$secretData->secretVersion = $version;
					}
				}
			}
			else
			{
				if($arrayLength != 2){
					return null;
				}else{
					$version = $dataArray[1];
					if(is_numeric($version))
					{
						$secretData->originalBase64Value = $dataArray[0];
						$secretData->secretVersion = $version;
					}
				}
			}
			return $secretData;
		}

		function getIndexSecretData($data,$separator) {
			$secretData = new SecretData;
			$dataArray = explode($separator,$this->trimBySep($data,$separator));
			$arrayLength = count($dataArray);

	        if($separator == PHONE_SEPARATOR_CHAR) {
	            if ($arrayLength != 3) {
	                return null;
	            }else{
					$version = $dataArray[2];
					if(is_numeric($version))
					{
						$secretData->originalValue = $dataArray[0];
						$secretData->originalBase64Value = $dataArray[1];
						$secretData->secretVersion = $version;
					}
	            }
	            
	        } else {
	        	if($arrayLength != 3){
					return null;
				} else {
					$version = $dataArray[2];
					if(is_numeric($version))
					{
						$secretData->originalBase64Value = $dataArray[0];
						$secretData->originalValue = $dataArray[1];
						$secretData->secretVersion = $version;
					}
				}
	        }

	        $secretData->search = true;
	        return $secretData;
		}

		/**
	     * 判断密文是否支持检索
	     * 
	     * @param key
	     * @param version
	     * @return
	     */
		function isIndexEncrypt($key,$version,$secretContext)
		{
	        if ($version != null && $version < 0) {
	            $key = "previous_".$key;
	        } else {
	            $key = "current_".$key;
	        }

	        return $secretContext->appConfig != null && 
	               array_key_exists($key,$secretContext->appConfig) && 
	               $secretContext->appConfig[$key] == "2";
		}

		function isLetterOrDigit($ch)
		{
			$code = ord($ch);
			if (0 <= $code && $code <= 127) {
            	return true;
        	}
        	return false;
		}

		function utf8_strlen($string = null) {
			// 将字符串分解为单元
			preg_match_all("/./us", $string, $match);
			// 返回单元个数
			return count($match[0]);
		}

		function utf8_substr($string,$start,$end) {
			// 将字符串分解为单元
			preg_match_all("/./us", $string, $match);
			// 返回单元个数
			$result = "";
			for($i = $start; $i < $end; $i++){
				$result .= $match[0][$i];
			}
			return $result;
		}

		function utf8_str_at($string,$index) {
			// 将字符串分解为单元
			preg_match_all("/./us", $string, $match);
			// 返回单元个数
			return $match[0][$index];
		}

		function compress($input,$toLength) {
			if($toLength < 0) {
				return null;
			}
			$output = array();
			for($i = 0; $i < $toLength; $i++) {
				$output[$i] = chr(0);
			}
			$input = $this->getBytes($input);
			$inputLength = count($input);
			for ($i = 0; $i < $inputLength; $i++) {
	            $index_output = $i % $toLength;
	            $output[$index_output] = $output[$index_output] ^ $input[$i];
	        }
	        return $output;
		}

	    /**
	     * @see #hmacMD5Encrypt
	     * 
	     * @param encryptText
	     *            被签名的字符串
	     * @param encryptKey
	     *            密钥
	     * @param compressLen压缩长度
	     * @return
	     * @throws Exception
	     */
		function hmacMD5EncryptToBase64($encryptText,$encryptKey,$compressLen = 0) {
			$encryptResult = Security::hmac_md5($encryptText,$encryptKey);
			if($compressLen != 0){
				$encryptResult = $this->compress($encryptResult,$compressLen);
			}
			return base64_encode($this->toStr($encryptResult));
		}


	    /**
	     * 生成滑动窗口
	     * 
	     * @param input
	     * @param slideSize
	     * @return
	     */
		function getSlideWindows($input,$slideSize = 4)
		{
			$endIndex = 0;
			$startIndex = 0;
			$currentWindowSize = 0;
			$currentWindow = null;
			$dataLength = $this->utf8_strlen($input);
			$windows = array();
			while($endIndex < $dataLength || $currentWindowSize > $slideSize) 
			{
				$startsWithLetterOrDigit = false;
				if(!empty($currentWindow)){
					$startsWithLetterOrDigit = $this->isLetterOrDigit($this->utf8_str_at($currentWindow,0));
				}
				if($endIndex == $dataLength && $startsWithLetterOrDigit == false){
					break;
				}
				if($currentWindowSize == $slideSize && 
				   $startsWithLetterOrDigit == false && 
				   $this->isLetterOrDigit($this->utf8_str_at($input,$endIndex))) {
				   $endIndex ++;
				   $currentWindow = $this->utf8_substr($input,$startIndex,$endIndex);
				   $currentWindowSize = 5;
				} else {
				    if($endIndex != 0){
				    	if($startsWithLetterOrDigit){
				    		$currentWindowSize -= 1;
				    	}else{
				    		$currentWindowSize -= 2;
				    	}
				    	$startIndex ++;
				    }

	                while ($currentWindowSize < $slideSize && $endIndex < $dataLength) {
	                    $currentChar = $this->utf8_str_at($input,$endIndex);
	                    if ($this->isLetterOrDigit($currentChar)) {
	                        $currentWindowSize += 1;
	                    } else {
	                        $currentWindowSize += 2;
	                    }
	                    $endIndex++;
	                }
	                $currentWindow = $this->utf8_substr($input,$startIndex,$endIndex);
				}
				array_push($windows,$currentWindow);
			}
			return $windows;
		}

		function encryptPhoneIndex($data,$separator,$secretContext) {
			$dataLength = strlen($data);
			if($dataLength < 11) {
				return $data;
			}
			$last4Number = substr($data,$dataLength -4 ,$dataLength);
			return $separator.$this->hmacMD5EncryptToBase64($last4Number,$secretContext->secret).$separator
				   .Security::encrypt($data,$secretContext->secret).$separator.$secretContext->secretVersion
				   .$separator.$separator;
		}

		function encryptNormalIndex($data,$compressLen,$slideSize,$separator,$secretContext) {
			$slideList = $this->getSlideWindows($data, $slideSize);
			$builder = "";
			foreach ($slideList as $slide) {
				$builder .= $this->hmacMD5EncryptToBase64($slide,$secretContext->secret,$compressLen);
			}
			return $separator.Security::encrypt($data,$secretContext->secret).$separator.$builder.$separator
				   .$secretContext->secretVersion.$separator.$separator;
		}

		function getArrayValue($array,$key,$default) {
			if(array_key_exists($key, $array)){
				return $array[$key];
			}
			return $default;
		}

		function getBytes($string) {  
	        $bytes = array();  
	        for($i = 0; $i < strlen($string); $i++){  
	             $bytes[] = ord($string[$i]);  
	        }  
	        return $bytes;  
	    }  

		function toStr($bytes) {
			if(!is_array($bytes)){
				return $bytes;
			}
	        $str = '';	        
	        foreach($bytes as $ch) {
	            $str .= chr($ch);
	        }
	        return $str;
	    }
	}
?>