/* ************************************************************************
#
#  DivConq
#
#  http://divconq.com/
#
#  Copyright:
#    Copyright 2012 eTimeline, LLC. All rights reserved.
#
#  License:
#    See the license.txt file in the project's top-level directory for details.
#
#  Authors:
#    * Andy White
#
************************************************************************ */
package divconq.util;

import java.security.InvalidKeyException;
import java.security.SecureRandom;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import divconq.lang.chars.Utf8Decoder;
import divconq.lang.chars.Utf8Encoder;
import divconq.log.Logger;
import divconq.xml.XElement;

/**
 * This is the default settings obfuscator, see ISettingsObfuscator for hints on 
 * how to build your own.  If a customer obfuscator is not provided then all
 * encrypted configuration settings use this implementation.
 * 
 *  This is good enough to keep the casual hacker at bay.  The encryption key is
 *  created by a combination of settings in config.xml and a hard coded default salt.
 *  So to break an encryption the hacker needs both the code and the config file.
 * 
 * @author Andy
 *
 */
public class BasicSettingsObfuscator implements ISettingsObfuscator {
	public static final byte[] DEFAULT_SALT = {
		(byte)201, (byte) 15, (byte)218, (byte)162, (byte) 33, (byte)104, (byte)194, (byte) 52,
		(byte)196, (byte)198, (byte) 98, (byte)139, (byte)128, (byte)220, (byte) 28, (byte)209,
		(byte) 41, (byte)  2, (byte) 78, (byte)  8, (byte)138, (byte)103, (byte)204, (byte)116,
		(byte)  2, (byte) 11, (byte)190, (byte)166, (byte) 59, (byte) 19, (byte)155, (byte) 34,
		(byte) 81, (byte) 74, (byte)  8, (byte)121, (byte)142, (byte) 52, (byte)  4, (byte)221,
		(byte)239, (byte)149, (byte) 25, (byte)179, (byte)205, (byte) 58, (byte) 67, (byte) 27,
		(byte) 48, (byte) 43, (byte) 10, (byte)109, (byte)242, (byte) 95, (byte) 20, (byte) 55,
		(byte) 79, (byte)225, (byte) 53, (byte)109, (byte)109, (byte) 81, (byte)194, (byte) 69
	};
	
	protected byte[] masterkey = null;
	protected SecretKeySpec aeskey = null;
	protected SecretKeySpec hmackey = null;
	
	/* (non-Javadoc)
	 * @see divconq.util.ISettingsObfuscator#init(divconq.xml.XElement)
	 */
	@Override
	public void init(XElement config) {
		String salt1 = null;
		String salt2 = null;
		
		if (config != null) {
			salt1 = config.getAttribute("Id");
			salt2 = config.getAttribute("Feed");
		}
		
		byte[] skey = new byte[128];
		
		if (StringUtil.isEmpty(salt1)) 
			salt1 = "48656c6c6f";
		else if (salt1.length() > 128)
			salt1 = salt1.substring(salt1.length() - 128);
	
		byte[] bsalt1 = HexUtil.decodeHex(salt1);
		
		if (bsalt1 == null)
			bsalt1 = DEFAULT_SALT;
		
		ArrayUtil.blockCopy(bsalt1, 0, skey, 128 - bsalt1.length, bsalt1.length);
		
		if (bsalt1.length < 64) 
			ArrayUtil.blockCopy(DEFAULT_SALT, 0, skey, 64, 64 - bsalt1.length);
		
		if (StringUtil.isEmpty(salt2)) 
			salt2 = "576f726c64";
		else if (salt2.length() > 128)
			salt2 = salt2.substring(salt2.length() - 128);
		
		byte[] bsalt2 = HexUtil.decodeHex(salt2);
		
		if (bsalt2 == null)
			bsalt2 = DEFAULT_SALT;
		
		ArrayUtil.blockCopy(bsalt2, 0, skey, 0, bsalt2.length);
		
		if (bsalt2.length < 64) 
			ArrayUtil.blockCopy(DEFAULT_SALT, bsalt2.length, skey, bsalt2.length, 64 - bsalt2.length);
		
		this.masterkey = skey;
		
		byte[] akey = new byte[16];
		ArrayUtil.blockCopy(skey, bsalt2.length - 10, akey, 0, 16);
		
		this.aeskey = new SecretKeySpec(akey, "AES");
		this.hmackey = new SecretKeySpec(skey, "hmacSHA512");
	}

	/* (non-Javadoc)
	 * @see divconq.util.ISettingsObfuscator#configure(divconq.xml.XElement)
	 */
	@Override
	public void configure(XElement config) {
		byte[] idbuff = new byte[64];
		byte[] feedbuff = new byte[64];
		
		SecureRandom rnd = new SecureRandom();
		rnd.nextBytes(idbuff);
		rnd.nextBytes(feedbuff);
		
		config.setAttribute("Id", HexUtil.bufferToHex(idbuff));
		config.setAttribute("Feed", HexUtil.bufferToHex(feedbuff));
	}

	/* (non-Javadoc)
	 * @see divconq.util.ISettingsObfuscator#decryptHexToString(java.lang.CharSequence)
	 */
	@Override
	public String decryptHexToString(CharSequence v) {
		return this.decryptString(HexUtil.decodeHex(v));
	}

	/* (non-Javadoc)
	 * @see divconq.util.ISettingsObfuscator#decryptString(byte[])
	 */
	@Override
	public String decryptString(byte[] v) {
		if (v == null)
			return null;
		
		try {
			Cipher c = Cipher.getInstance("AES");
			c.init(Cipher.DECRYPT_MODE, this.aeskey);
			return Utf8Decoder.decode(c.doFinal(v)).toString();			
		}
		catch(InvalidKeyException x) {
			Logger.warn("Invalid settings key: " + x, "Code", "202");
		}
		catch(Exception x) {
			Logger.info("Failed decryption: " + x, "Code", "203");
		}
		
		return null;
	}

	/* (non-Javadoc)
	 * @see divconq.util.ISettingsObfuscator#encryptStringToHex(java.lang.CharSequence)
	 */
	@Override
	public String encryptStringToHex(CharSequence v) {
    	return HexUtil.bufferToHex(this.encryptString(v));
	}

	/* (non-Javadoc)
	 * @see divconq.util.ISettingsObfuscator#encryptString(java.lang.CharSequence)
	 */
	@Override
	public byte[] encryptString(CharSequence v) {
		if (StringUtil.isEmpty(v))
			return null;
		
		try {
			Cipher c = Cipher.getInstance("AES");
			c.init(Cipher.ENCRYPT_MODE, this.aeskey);
			return c.doFinal(Utf8Encoder.encode(v));				
		}
		catch(InvalidKeyException x) {
			Logger.warn("Invalid settings key: " + x, "Code", "204");
		}
		catch(Exception x) {
			Logger.info("Failed decryption: " + x, "Code", "205");
		}
		
		return null;
	}

	/* (non-Javadoc)
	 * @see divconq.util.ISettingsObfuscator#hashStringToHex(java.lang.CharSequence)
	 */
	@Override
	public String hashStringToHex(CharSequence v) {
    	return HexUtil.bufferToHex(this.hashString(v));
	}

	/* (non-Javadoc)
	 * @see divconq.util.ISettingsObfuscator#hashString(java.lang.CharSequence)
	 */
	@Override
	public byte[] hashString(CharSequence v) {
		if (StringUtil.isEmpty(v))
			return null;
		
		try {
			Mac mac = Mac.getInstance("hmacSHA512");
			mac.init(this.hmackey);			
			return mac.doFinal(Utf8Encoder.encode(v));
		} 
		catch (Exception x) {
			Logger.info("Failed hash: " + x, "Code", "206");
		}	
		
		return null;
	}
}
