/******************************************************************************
 * Product: Adempiere ERP & CRM Smart Business Solution                       *
 * Copyright (C) 1999-2006 ComPiere, Inc. All Rights Reserved.                *
 * This program is free software; you can redistribute it and/or modify it    *
 * under the terms version 2 of the GNU General Public License as published   *
 * by the Free Software Foundation. This program is distributed in the hope   *
 * that it will be useful, but WITHOUT ANY WARRANTY; without even the implied *
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.           *
 * See the GNU General Public License for more details.                       *
 * You should have received a copy of the GNU General Public License along    *
 * with this program; if not, write to the Free Software Foundation, Inc.,    *
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.                     *
 * For the text or an alternative of this public license, you may reach us    *
 * ComPiere, Inc., 2620 Augustine Dr. #245, Santa Clara, CA 95054, USA        *
 * or via info@compiere.org or http://www.compiere.org/license.html           *
 *****************************************************************************/
package org.compiere.util;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.util.Properties;
import java.util.logging.Level;
/**
 *  Secure engine for encryption and decryption
 *	
 *  @author Jorg Janke
 *  @version $Id: SecureEngine.java,v 1.2 2006/07/30 00:52:23 jjanke Exp $
 */
public class SecureEngine
{
	/**
	 * 	Initialize Security
	 *	@param ctx context with ADEMPIERE_SECURE class name
	 */
	public static void init (Properties ctx)
	{
		if (s_engine == null)
		{
			String className = ctx.getProperty(SecureInterface.ADEMPIERE_SECURE);
			s_engine = new SecureEngine(className);
		}
	}	//	init
	
	/**
	 * 	Initialize/Test Security
	 *	@param className class name
	 */
	public static void init (String className)
	{
		if (s_engine == null)
			s_engine = new SecureEngine(className);
		else if (className != null && !className.equals(getClassName()))
		{
			String msg = "Requested Security class = " + className
				+ " is not the same as the active class = " + getClassName()
				+ "\nMake sure to set the security class in the start script";
			log.severe(msg);
			System.err.println(msg);
			System.exit(10);
		}
	}	//	init
	/**
	 * 	Get Class Name
	 *	@return class name 
	 */
	public static String getClassName()
	{
		if (s_engine == null)
			return null;
		return s_engine.implementation.getClass().getName();
	}	//	getClassName
	
	/**
	 *  Convert String and salt to SHA-512 hash with iterations
	 *  https://www.owasp.org/index.php/Hashing_Java
	 *
	 *  @param value message
	 *  @return HexString of message (length = 128 characters)
	 *  @throws UnsupportedEncodingException 
	 *  @throws NoSuchAlgorithmException 
	 */
	public static String getSHA512Hash (int iterations, String value, byte[] salt) throws NoSuchAlgorithmException, UnsupportedEncodingException
	{
		if (s_engine == null)
			init(System.getProperties());
		return s_engine.implementation.getSHA512Hash(iterations, value, salt);
	}	//	getDigest	
	
	/**
	 *  Perform MD5 Digest of value.
	 *  JavaScript version see - http://pajhome.org.uk/crypt/md5/index.html
	 *
	 *  @param value message
	 *  @return HexString of digested message (length = 32 characters)
	 */
	public static String getDigest (String value)
	{
		if (s_engine == null)
			init(System.getProperties());
		return s_engine.implementation.getDigest(value);
	}	//	getDigest
	
	/**
	 *	Encryption.
	 * 	The methods must recognize clear text values
	 *  @param value clear value
	 *  @param AD_Client_ID
	 *  @return encrypted String
	 */
	public static String encrypt (String value,int AD_Client_ID)
	{
		if (value == null || value.length() == 0)
			return value;
		if (s_engine == null)
			init(System.getProperties());
		//
		boolean inQuotes = value.startsWith("'") && value.endsWith("'");
		if (inQuotes)
			value = value.substring(1, value.length()-1);
		//
		String retValue = s_engine.implementation.encrypt(value,AD_Client_ID);
		if (inQuotes)
			return "'" + retValue + "'";
		return retValue;
	}	//	encrypt
	
	/**
	 *	Decryption.
	 * 	The methods must recognize clear text values
	 *  @param value encrypted value
	 *  @param AD_Client_ID
	 *  @return decrypted String
	 */
	public static String decrypt (String value, int AD_Client_ID)
	{
		if (value == null)
			return null;
		if (s_engine == null)
			init(System.getProperties());
		boolean inQuotes = value.startsWith("'") && value.endsWith("'");
		if (inQuotes)
			value = value.substring(1, value.length()-1);
		String retValue = null;
		if (value.startsWith(SecureInterface.CLEARVALUE_START) && value.endsWith(SecureInterface.CLEARVALUE_END))
			retValue = value.substring(SecureInterface.CLEARVALUE_START.length(), value.length()-SecureInterface.CLEARVALUE_END.length());
		else
			retValue = s_engine.implementation.decrypt(value,AD_Client_ID);
		if (inQuotes)
			return "'" + retValue + "'";
		return retValue;
	}	//	decrypt
	
	/**
	 *	Encrypt value (only implemented for String).
	 *  @param value clear value
	 *  @param AD_Client_ID
	 *  @return encrypted String
	 */
	public static Object encrypt (Object value, int AD_Client_ID)
	{
		if (value instanceof String)
			return encrypt((String) value, AD_Client_ID);
		return value;
	}	//	encrypt
	/**
	 *	Decrypt value (only implemented for String)
	 *  @param value encrypted value
	 *  @return decrypted String
	 */
	public static Object decrypt (Object value, int AD_Client_ID)
	{
		if (value instanceof String)
			return decrypt((String) value, AD_Client_ID);
		return value;
	}	//	decrypt
	
	/**
	 * 	SecureEngine constructor
	 * 	@param className class name if null defaults to org.compiere.util.Secure 
	 */
	private SecureEngine (String className)
	{
		String realClass = className;
		if (realClass == null || realClass.length() == 0)
			realClass = SecureInterface.ADEMPIERE_SECURE_DEFAULT;
		Exception cause = null;
		try
		{
			Class> clazz = Class.forName(realClass);
			implementation = (SecureInterface)clazz.getDeclaredConstructor().newInstance();
		}
		catch (Exception e)
		{
			cause = e;
		}
		if (implementation == null)
		{
			String msg = "Could not initialize: " + realClass + " - " + cause.toString()
				+ "\nCheck start script parameter ADEMPIERE_SECURE"; 
			log.severe(msg);
			System.err.println(msg);
			System.exit(10);
		}
		//	See if it works
		String testE = implementation.encrypt(TEST,0);
		String testC = implementation.decrypt(testE,0);
		if (!testC.equals(TEST))
			throw new IllegalStateException(realClass 
				+ ": " + TEST
				+ "->" + testE + "->" + testC);
		if (log.isLoggable(Level.CONFIG)) log.config (realClass + " initialized - " + implementation);
	}	//	SecureEngine
	/**
	 * Use salt in hex form and text hashed compare with plan text.
	 * If has exception in hash, log to server.
	 * @param hashedText
	 * @param hexSalt
	 * @param planText
	 * @return true if valid
	 */
	public static boolean isMatchHash (String hashedText, String hexSalt, String planText){
		boolean valid=false;
		// always do calculation to prevent timing based attacks
		if ( hashedText == null )
			hashedText = "0000000000000000";
		if ( hexSalt == null )
			hexSalt = "0000000000000000";
		try {
			valid= SecureEngine.getSHA512Hash(1000, planText, Secure.convertHexString(hexSalt)).equals(hashedText);
		} catch (NoSuchAlgorithmException ignored) {
			log.log(Level.WARNING, "Password hashing not supported by JVM");
		} catch (UnsupportedEncodingException ignored) {
			log.log(Level.WARNING, "Password hashing not supported by JVM");
		}
				
	 	return valid;
	}
	
	/** Test String					*/
	private static final String	TEST = "This is a 0123456789 .,; -= Test!";
	/** Secure Engine 				*/
	private volatile static SecureEngine	s_engine = null;	
	
	/** The real Engine				*/
	private	SecureInterface		implementation = null;
	/**	Logger						*/
	private static CLogger		log	= CLogger.getCLogger (SecureEngine.class.getName());
			
}	//	SecureEngine