/*
 * Copyright 2014-2016 Gotham Digital Science LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package com.gdssecurity.anticsrf.protections;

import java.sql.Timestamp;
import java.util.Date;
import java.util.logging.Logger;

import org.keyczar.Signer;
import org.keyczar.exceptions.KeyczarException;

import com.gdssecurity.anticsrf.exceptions.CSRFTokenGenerationException;
import com.gdssecurity.anticsrf.exceptions.CSRFTokenVerificationException;
import com.gdssecurity.anticsrf.utils.ConfigUtil;
import com.gdssecurity.anticsrf.utils.Constants;
import com.gdssecurity.anticsrf.utils.StringUtil;
import com.gdssecurity.anticsrf.utils.KeyczarWrapper;

public class HMACCSRFProtection implements CSRFProtection {

	private static final Logger LOG = Logger.getLogger(HMACCSRFProtection.class.getName());
	
	private String userSeed;
	
	public HMACCSRFProtection(String userSeed) 
	{
		this.userSeed = userSeed;
	}
	
	@Override
	public boolean verifyCSRFToken(String url, String tokenFromUser) throws CSRFTokenVerificationException {
		return verifyCSRFToken(url, tokenFromUser, ConfigUtil.hasUrlSpecificConfig(url));
	}

	@Override
	public String generateCSRFToken() throws CSRFTokenGenerationException {
		String csrfToken = handleCSRFTokenGeneration(this.userSeed);
		
		LOG.fine("Setting csrfToken: attrname=" + 
				ConfigUtil.getProp(Constants.CONF_TOKEN_REQATTR) +
				", csrftoken=" + csrfToken);
		return csrfToken;
	}

	@Override
	public String getCSRFTokenParameterName() {
		return ConfigUtil.getProp(Constants.CONF_TOKEN_PARAM);
	}

	@Override
	public String generateUrlSpecificCSRFToken(String url)
			throws CSRFTokenGenerationException {
		return this.generateCSRFToken(userSeed+":"+url);
	}
	
	private String generateCSRFToken(String userSeed) throws CSRFTokenGenerationException {
		String csrfToken = handleCSRFTokenGeneration(userSeed);
		
		LOG.info("Setting csrfToken: attrname=" + 
				ConfigUtil.getProp(Constants.CONF_TOKEN_REQATTR) +
				", csrftoken=" + csrfToken);
		return csrfToken;
	}
	
	private boolean verifyCSRFToken(String url, String token, boolean isUrlSpecific, Long timeout) throws CSRFTokenVerificationException
	{
		// Get  CSRF User Seed from request attribute and set the default timeout
		StringBuffer userSeed = new StringBuffer(this.userSeed);
		
		if(isUrlSpecific)
		{
			// Add the current url to the seed and pass in it's configured timeout
			userSeed.append( ":" + url );
			LOG.fine("Using URL Specific timeout values");
		}
		
		return handleCSRFTokenVerification(url, token, userSeed.toString(), timeout);
	}
	
	private boolean handleCSRFTokenVerification(String url, String submittedCSRFToken, String userSeed, Long configuredTimeout ) throws CSRFTokenVerificationException
	{
		if( ConfigUtil.isURLExempt( url ) )
		{
			return true;
		}
		
		try 
		{
			if( submittedCSRFToken == null)
			{
				String err = "HttpServletRequest is missing CSRFToken Parameter." +
							"userSeed=" + StringUtil.stripNewlines(userSeed);
				LOG.warning(err);
				return false;
			}
			
			String[] csrfTokenContents = submittedCSRFToken.split(":");
	
			if( csrfTokenContents.length != 2 ) 
			{
				LOG.warning("CSRF Token contains invalid amount of delimiters. "+
						"userSeed=" + StringUtil.stripNewlines(userSeed) +
						", submittedToken= " + StringUtil.stripNewlines(submittedCSRFToken));
				return false;
			}
			
			String submittedHmac = csrfTokenContents[0];
			String submittedTimestamp = csrfTokenContents[1];
			
			// Get Keyczar Signer object
			KeyczarWrapper keyczarWrapper = ConfigUtil.getKeyczarWrapper();
			Signer csrfSigner = keyczarWrapper.getCSRFSigner();
			
			if( !csrfSigner.verify(userSeed + ":" + submittedTimestamp, submittedHmac) )
			{
				LOG.warning("Submitted CSRF Token did not contain a valid HMAC signature. "+
						"userSeed=" + StringUtil.stripNewlines(userSeed) +
						", submittedToken=" + StringUtil.stripNewlines(submittedCSRFToken));
				return false;
			}
			
			if( timestampIsExpired( Long.valueOf(submittedTimestamp), configuredTimeout) )
			{
				LOG.warning("Submitted CSRF Token is expired. "+
						"userSeed=" + StringUtil.stripNewlines(userSeed) +
						", submittedToken= " + StringUtil.stripNewlines(submittedCSRFToken));
				return false;
			}
			
			// We passed all the checks.
			return true;
		} 
		catch( KeyczarException ex ) 
		{
			String err = "Encountered error performing HMAC signature validation with the Keyczar library";
					
			// Logging a warning here since this exception is caught and handled by the filter. This should
			// should be considered a security warning. 
			LOG.warning(err+ ", userSeed=" + StringUtil.stripNewlines(userSeed) +
					", submittedToken=" + StringUtil.stripNewlines(submittedCSRFToken) +
					", exception=" + ex.getMessage() );
			throw new CSRFTokenVerificationException(err);
		}
		catch( NumberFormatException ex )
		{
			String err = "Timestamp submitted within CSRFToken is not in a valid format";

			LOG.warning(err + ", Submitted CSRFToken="+
					submittedCSRFToken + ", userSeed=" + userSeed);
			throw new CSRFTokenVerificationException(err);
		}
	}
	
	private boolean timestampIsExpired( Long submittedTimestamp, Long configuredTimeout )
	{
		try 
		{
			Date current = new Date();

			Timestamp currentTime = new Timestamp( current.getTime() );
			Timestamp timeToCompare = new Timestamp(submittedTimestamp);

			Long diff = currentTime.getTime() - timeToCompare.getTime();
			Long diffInSeconds = diff / 1000; // 1,000 MilliSecs in a second 
			
			if (diffInSeconds > configuredTimeout) 
			{
				return true;
			}
		} 
		catch (Exception e) 
		{
			//log severe?
			LOG.severe("Unexpected error occurred during CSRFToken timestamp verification, " +
					"exceptionmessage=" + e.getMessage());
			return true;
		}
		
		return false;
	}
	
	private boolean verifyCSRFToken(String url, String tokenFromUser, boolean isUrlSpecific) throws CSRFTokenVerificationException
	{
		Long timeout = (isUrlSpecific 
				? ConfigUtil.getUrlSpecificConfig(url) 
				: Long.valueOf( ConfigUtil.getProp(Constants.CONF_HMAC_SITEWIDE_TIMEOUT) ));
		
		return verifyCSRFToken(url, tokenFromUser, isUrlSpecific, timeout);
	}
	
	private String handleCSRFTokenGeneration(String unhashedToken) throws CSRFTokenGenerationException
	{
		try
		{			
			Date currentTime = new Date();
			String currentTimeString = String.valueOf( currentTime.getTime() );
			KeyczarWrapper keyczarWrapper = ConfigUtil.getKeyczarWrapper();
			Signer csrfSigner = keyczarWrapper.getCSRFSigner();
			String csrfHmac = csrfSigner.sign(unhashedToken + ":" + currentTimeString);
			
			return csrfHmac + ":" + currentTimeString;
		}
		catch( KeyczarException ex ) 
		{
			String err = "Encountered error creating HMAC signature with the Keyczar library"
					+ ", exceptionmessage=" + ex.getMessage();
			LOG.info(err);
			throw new CSRFTokenGenerationException(err);
		}
	}

}