/*
 * Created on 15 Jun 2006
 * Created by Paul Gardner
 * Copyright (C) 2006 Aelitis, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 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.
 * 
 * AELITIS, SAS au capital de 46,603.30 euros
 * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
 *
 */

package com.aelitis.azureus.core.security.impl;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;

import org.bouncycastle.jce.provider.JCEIESCipher;
import org.bouncycastle.jce.spec.IEKeySpec;
import org.bouncycastle.jce.spec.IESParameterSpec;
import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.util.Base32;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.RandomUtils;
import org.gudy.azureus2.core3.util.SystemTime;

import com.aelitis.azureus.core.security.CryptoECCUtils;
import com.aelitis.azureus.core.security.CryptoHandler;
import com.aelitis.azureus.core.security.CryptoManager;
import com.aelitis.azureus.core.security.CryptoManagerException;
import com.aelitis.azureus.core.security.CryptoManagerPasswordException;
import com.aelitis.azureus.core.security.CryptoManagerPasswordHandler;
import com.aelitis.azureus.core.security.CryptoSTSEngine;

public class 
CryptoHandlerECC
	implements CryptoHandler
{	
	private static final String	DEFAULT_PASSWORD	= "";
	private static final Long	DEFAULT_TIMEOUT		= Long.MAX_VALUE;
	

	private static final int	TIMEOUT_DEFAULT_SECS		= 60*60;

	
	private CryptoManagerImpl		manager;
	
	private String				CONFIG_PREFIX = CryptoManager.CRYPTO_CONFIG_PREFIX + "ecc.";

	private PrivateKey			use_method_private_key;
	private PublicKey			use_method_public_key;
	
	private long	last_unlock_time;
	
	protected
	CryptoHandlerECC(
		CryptoManagerImpl		_manager,
		int						_instance_id )
	{
		manager	= _manager;
		
		CONFIG_PREFIX += _instance_id + ".";
		
			// migration away from system managed keys
				
		if ( getDefaultPasswordHandlerType() != CryptoManagerPasswordHandler.HANDLER_TYPE_USER ){
		
			COConfigurationManager.setParameter( CONFIG_PREFIX + "default_pwtype", CryptoManagerPasswordHandler.HANDLER_TYPE_USER );
		}
		
		if ( 	getCurrentPasswordType() == CryptoManagerPasswordHandler.HANDLER_TYPE_SYSTEM || 
				COConfigurationManager.getByteParameter( CONFIG_PREFIX + "publickey", null ) == null ){
			
			try{
				createAndStoreKeys(
					manager.setPassword(
						CryptoManager.HANDLER_ECC,
						CryptoManagerPasswordHandler.HANDLER_TYPE_USER,
						DEFAULT_PASSWORD.toCharArray(),
						DEFAULT_TIMEOUT ));
				
				Debug.outNoStack( "Successfully migrated key management" );
				
			}catch( Throwable e ){
				
				Debug.out( "Failed to migrate key management", e );
			}
		}
	}
	
	public int
	getType()
	{
		return( CryptoManager.HANDLER_ECC );
	}
	
	public void
	unlock()
	
		throws CryptoManagerException
	{
		getMyPrivateKey( "unlock" );
	}
	
	public synchronized boolean
	isUnlocked()
	{
		return( use_method_private_key != null );
	}
	
	public void
	lock()
	{
		boolean	changed = false;
		
		synchronized( this ){
			
			changed = use_method_private_key != null;
		
			use_method_private_key	= null;
		}
		
		if ( changed ){
		
			manager.lockChanged( this );
		}
	}
	
	public int
	getUnlockTimeoutSeconds()
	{
		return( COConfigurationManager.getIntParameter( CONFIG_PREFIX + "timeout", TIMEOUT_DEFAULT_SECS ));
	}
	
	public void
	setUnlockTimeoutSeconds(
		int		secs )
	{
		COConfigurationManager.setParameter( CONFIG_PREFIX + "timeout", secs );
	}
		
	public byte[]
	sign(
		byte[]		data,
		String		reason )
	
		throws CryptoManagerException
	{
		PrivateKey	priv = getMyPrivateKey( reason );
		
		Signature sig = CryptoECCUtils.getSignature( priv );
		
		try{
			sig.update( data );
			
			return( sig.sign());
			
		}catch( Throwable e ){
			
			throw( new CryptoManagerException( "Signature failed", e ));
		}
	}
	 
	public boolean
	verify(
		byte[]		public_key,
		byte[]		data,
		byte[]		signature )
	
		throws CryptoManagerException
	{
		PublicKey	pub = CryptoECCUtils.rawdataToPubkey( public_key );
		
		Signature sig = CryptoECCUtils.getSignature( pub );
		
		try{
			sig.update( data );
			
			return( sig.verify( signature ));
			
		}catch( Throwable e ){
			
			throw( new CryptoManagerException( "Signature failed", e ));
		}
	}
	
	public byte[]
	encrypt(
		byte[]		other_public_key,
		byte[]		data,
		String		reason )
		
		throws CryptoManagerException
	{	        
		try{
			IEKeySpec   key_spec = new IEKeySpec( getMyPrivateKey( reason ), CryptoECCUtils.rawdataToPubkey( other_public_key ));
	 
			byte[]	d = new byte[16];
			byte[]	e = new byte[16];
			
			RandomUtils.nextSecureBytes( d );
			RandomUtils.nextSecureBytes( e );
			
			IESParameterSpec param = new IESParameterSpec( d, e, 128);
		
			InternalECIES	cipher = new InternalECIES();
	
			cipher.internalEngineInit( Cipher.ENCRYPT_MODE, key_spec, param, null ); 
		
			byte[]	encrypted = cipher.internalEngineDoFinal(data, 0, data.length );
			
			byte[] result = new byte[32+encrypted.length];
			
			System.arraycopy( d, 0, result, 0, 16 );
			System.arraycopy( e, 0, result, 16, 16 );
			System.arraycopy( encrypted, 0, result, 32, encrypted.length );
			
			return( result );
			
		}catch( CryptoManagerException e ){
			
			throw( e );
			
		}catch( Throwable e){
			
			throw( new CryptoManagerException( "Encrypt failed", e ));
		}
	}
	
	public byte[]
	decrypt(
		byte[]		other_public_key,
		byte[]		data,
		String		reason )
		
		throws CryptoManagerException
	{	        
		try{
			IEKeySpec   key_spec = new IEKeySpec( getMyPrivateKey(  reason ), CryptoECCUtils.rawdataToPubkey( other_public_key ));
	 	
			byte[]	d = new byte[16];
			byte[]	e = new byte[16];
			
			System.arraycopy( data, 0, d, 0, 16 );
			System.arraycopy( data, 16, e, 0, 16 );
			
			IESParameterSpec param = new IESParameterSpec( d, e, 128);
		
			InternalECIES	cipher = new InternalECIES();
	
			cipher.internalEngineInit( Cipher.DECRYPT_MODE, key_spec, param, null ); 
		
			return( cipher.internalEngineDoFinal( data, 32, data.length - 32 ));
			
		}catch( CryptoManagerException e ){
			
			throw( e );
			
		}catch( Throwable e){
			
			throw( new CryptoManagerException( "Decrypt failed", e ));
		}
	}
		
	public CryptoSTSEngine
	getSTSEngine(
		String		reason )
	
		throws CryptoManagerException
	{
		return( new CryptoSTSEngineImpl( getMyPublicKey(  reason, true ), getMyPrivateKey( reason )));
	}
	
	public byte[]
	peekPublicKey()
	{
		try{
		
			return( CryptoECCUtils.keyToRawdata( getMyPublicKey( "peek", false )));
			
		}catch( Throwable e ){
			
			return( null );
		}
	}
	
	public byte[]
	getPublicKey(
		String		reason )
	
		throws CryptoManagerException
	{
		return( CryptoECCUtils.keyToRawdata( getMyPublicKey( reason, true )));
	}
  
	public byte[]
	getEncryptedPrivateKey(
		String		reason )
	
		throws CryptoManagerException
	{
		getMyPrivateKey( reason );
		
		byte[]	pk = COConfigurationManager.getByteParameter( CONFIG_PREFIX + "privatekey", null );

		if ( pk == null ){
			
			throw( new CryptoManagerException( "Private key unavailable" ));
		}
		
		int	pw_type = getCurrentPasswordType();
		
		byte[] res = new byte[pk.length+1];
		
		res[0] = (byte)pw_type;
		
		System.arraycopy( pk, 0, res, 1, pk.length );
		
		return( res );
	}
	
	public void
	recoverKeys(
		byte[]		public_key,
		byte[]		encrypted_private_key_and_type )
	
		throws CryptoManagerException
	{
		boolean	lock_changed = false;
		
		synchronized( this ){
			
			lock_changed = use_method_private_key != null;
			
			use_method_private_key	= null;
			use_method_public_key	= null;
			
			manager.clearPassword( CryptoManager.HANDLER_ECC, CryptoManagerPasswordHandler.HANDLER_TYPE_ALL );
			
			COConfigurationManager.setParameter( CONFIG_PREFIX + "publickey", public_key );
				
			int	type = (int)encrypted_private_key_and_type[0]&0xff;
			
			COConfigurationManager.setParameter( CONFIG_PREFIX + "pwtype", type );
	
			byte[] encrypted_private_key = new byte[encrypted_private_key_and_type.length-1];
			
			System.arraycopy( encrypted_private_key_and_type, 1, encrypted_private_key, 0, encrypted_private_key.length );
			
			COConfigurationManager.setParameter( CONFIG_PREFIX + "privatekey", encrypted_private_key );
			
			COConfigurationManager.save();
		}		
		
		manager.keyChanged( this );

		if ( lock_changed ){
		
			manager.lockChanged( this );
		}
	}
	
	public void
	resetKeys(
		String		reason )
	
		throws CryptoManagerException
	{
		boolean	lock_changed = false;

		synchronized( this ){
			
			lock_changed = use_method_private_key != null;

			use_method_private_key	= null;
			use_method_public_key	= null;
			
			manager.clearPassword( CryptoManager.HANDLER_ECC, CryptoManagerPasswordHandler.HANDLER_TYPE_ALL );
			
			COConfigurationManager.removeParameter( CONFIG_PREFIX + "publickey" );
				
			COConfigurationManager.removeParameter( CONFIG_PREFIX + "privatekey" );
			
			COConfigurationManager.save();
		}
		
		if ( lock_changed ){

			manager.lockChanged( this );
		}
		
		try{
		
			createAndStoreKeys( "resetting keys" );
			
		}catch( CryptoManagerException e ){
			
			manager.keyChanged( this );
				
			throw( e );
		}
	}
	
	protected PrivateKey
	getMyPrivateKey(
		String		reason )
	
		throws CryptoManagerException
	{
		boolean	lock_change = false;
		
		try{
			synchronized( this ){
				
				if ( use_method_private_key != null ){
					
					int	timeout_secs = getUnlockTimeoutSeconds();
					
					if ( timeout_secs > 0 ){
						
						if ( SystemTime.getCurrentTime() - last_unlock_time >= timeout_secs * 1000 ){
							
							lock_change = true;
							
							use_method_private_key = null;
						}
					}
				}
				
				if ( use_method_private_key != null ){
					
					return( use_method_private_key );
				}
			}
			
			final byte[]	encoded = COConfigurationManager.getByteParameter( CONFIG_PREFIX + "privatekey", null );
			
			if ( encoded == null ){
				
				return((PrivateKey)createAndStoreKeys( reason )[1]);
				
			}else{
				
				CryptoManagerImpl.passwordDetails password_details = 
					manager.getPassword( 
							CryptoManager.HANDLER_ECC, 
							CryptoManagerPasswordHandler.ACTION_DECRYPT, 
							reason,
							new CryptoManagerImpl.passwordTester()
							{
								public boolean 
								testPassword(
									char[] password )
								{
									try{
										manager.decryptWithPBE( encoded, password );
										
										return( true );
										
									}catch( Throwable e ){
										
										return( false );
									}
								}
							},
							getCurrentPasswordType());
	
				synchronized( this ){
					
					boolean		ok = false;
					
					try{
						use_method_private_key = CryptoECCUtils.rawdataToPrivkey( manager.decryptWithPBE( encoded, password_details.getPassword()));
					
						lock_change = true;
						
						last_unlock_time = SystemTime.getCurrentTime();
					
						if ( !checkKeysOK( reason )){
												
							throw( new CryptoManagerPasswordException( true, "Password incorrect" ));
						}
						
						ok = true;
						
					}catch( CryptoManagerException e ){
						
						throw( e );
						
					}catch( Throwable e ){
						
						throw( new CryptoManagerException( "Password incorrect", e ));
						
					}finally{
						
						if ( !ok ){
														
							manager.clearPassword( CryptoManager.HANDLER_ECC, CryptoManagerPasswordHandler.HANDLER_TYPE_ALL );
							
							lock_change = true;
							
							use_method_private_key	= null;
						}
					}
				}
			
				if ( use_method_private_key == null ){
					
					throw( new CryptoManagerException( "Failed to get private key" ));
				}
				
				return( use_method_private_key );
			}
		}finally{
			
			if ( lock_change ){

				manager.lockChanged( this );
			}
		}
	}
	
	protected boolean
	checkKeysOK(
		String	reason )
	
		throws CryptoManagerException
	{
		byte[]	test_data = "test".getBytes();
		
		return( verify( CryptoECCUtils.keyToRawdata( getMyPublicKey( reason, true )), test_data,  sign( test_data, reason )));
	}
	
	protected PublicKey
	getMyPublicKey(
		String		reason,
		boolean		create_if_needed )
	
		throws CryptoManagerException
	{
		boolean	create_new = false;
		
		synchronized( this ){
			
			if ( use_method_public_key == null ){
				
				byte[]	key_bytes = COConfigurationManager.getByteParameter( CONFIG_PREFIX + "publickey", null );
				
				if ( key_bytes == null ){
					
					if ( create_if_needed ){
						
						create_new = true;
						
					}else{
						
						return( null );
					}
				}else{
					
					use_method_public_key = CryptoECCUtils.rawdataToPubkey( key_bytes );
				}
			}
			
			if ( !create_new ){
				
				if ( use_method_public_key == null ){
					
					throw( new CryptoManagerException( "Failed to get public key" ));
				}
				
				return( use_method_public_key );
			}
		}
		
		return((PublicKey)createAndStoreKeys( reason )[0] );
	}
	
	public int
	getDefaultPasswordHandlerType()
	{
		return( COConfigurationManager.getIntParameter( CONFIG_PREFIX + "default_pwtype", CryptoManagerPasswordHandler.HANDLER_TYPE_USER ));
	}
	
	public void
	setDefaultPasswordHandlerType(
		int		new_type )
	
		throws CryptoManagerException
	{
		String reason = "Changing password handler";
		
		boolean	have_existing_keys = COConfigurationManager.getByteParameter( CONFIG_PREFIX + "privatekey", null ) != null;
		
			// ensure we unlock the private key so we can then re-persist it with new password

		if ( have_existing_keys ){
			
			if ( new_type == getCurrentPasswordType()){
				
				return;
			}
			
			getMyPrivateKey( reason );
		
			CryptoManagerImpl.passwordDetails password_details = 
				manager.getPassword( 
								CryptoManager.HANDLER_ECC,
								CryptoManagerPasswordHandler.ACTION_ENCRYPT,
								reason,
								null,
								new_type );
			
			
			synchronized( this ){
				
				if ( use_method_private_key == null ){
					
					throw( new CryptoManagerException( "Private key not available" ));
				}
				
				byte[]	priv_raw = CryptoECCUtils.keyToRawdata( use_method_private_key );
				
				byte[]	priv_enc = manager.encryptWithPBE( priv_raw, password_details.getPassword());
				
				COConfigurationManager.setParameter( CONFIG_PREFIX + "privatekey", priv_enc );
	
				COConfigurationManager.setParameter( CONFIG_PREFIX + "pwtype", password_details.getHandlerType());
	
				COConfigurationManager.setParameter( CONFIG_PREFIX + "default_pwtype", password_details.getHandlerType());
				
				COConfigurationManager.save();
			}
		}else{
			
				// not much to do as keys not yet created
			
			synchronized( this ){
				
				if ( COConfigurationManager.getByteParameter( CONFIG_PREFIX + "privatekey", null ) == null ){
					
					COConfigurationManager.setParameter( CONFIG_PREFIX + "default_pwtype", new_type );
				
					COConfigurationManager.save();
				}
			}
		}
	}
	
	protected Key[]
  	createAndStoreKeys(
  		String		reason )
  	
  		throws CryptoManagerException
  	{	
		CryptoManagerImpl.passwordDetails password_details = 
				manager.getPassword( 
  							CryptoManager.HANDLER_ECC,
  							CryptoManagerPasswordHandler.ACTION_ENCRYPT,
  							reason,
  							null,
  							getDefaultPasswordHandlerType());
		
		return( createAndStoreKeys( password_details ));
  	}
	
	protected Key[]
	createAndStoreKeys(
		CryptoManagerImpl.passwordDetails	password_details )
	
		throws CryptoManagerException
	{		
		try{
			synchronized( this ){
				
				if ( use_method_public_key == null || use_method_private_key == null ){
					
					KeyPair	keys = CryptoECCUtils.createKeys();
					
					use_method_public_key	= keys.getPublic();
					
					use_method_private_key	= keys.getPrivate();
					
					last_unlock_time = SystemTime.getCurrentTime();
					
					COConfigurationManager.setParameter( CONFIG_PREFIX + "publickey", CryptoECCUtils.keyToRawdata( use_method_public_key ));
					
					byte[]	priv_raw = CryptoECCUtils.keyToRawdata( use_method_private_key );
					
					byte[]	priv_enc = manager.encryptWithPBE( priv_raw, password_details.getPassword());
					
					COConfigurationManager.setParameter( CONFIG_PREFIX + "privatekey", priv_enc );
		
					COConfigurationManager.setParameter( CONFIG_PREFIX + "pwtype", password_details.getHandlerType());
		
					COConfigurationManager.save();
				}
				
				return( new Key[]{ use_method_public_key, use_method_private_key });
			}
		}finally{
				
			manager.keyChanged( this );
			
			manager.lockChanged( this );
		}
	}
	
	public boolean
	verifyPublicKey(
		byte[]	encoded )
	{
		try{
			CryptoECCUtils.rawdataToPubkey( encoded );
			
				// we can't actually verify the key size as although it should be 192 bits
				// it can be less due to leading bits being 0
			
			return( true );
			
		}catch( Throwable e ){
			
			return( false );
		}
	}
	
	public String
	exportKeys()
	
		throws CryptoManagerException
	{
		return( "id:      " + Base32.encode(manager.getSecureID()) + "\r\n" + 
				"public:  " + Base32.encode(getPublicKey( "Key export" )) + "\r\n" +
				"private: " + Base32.encode(getEncryptedPrivateKey( "Key export" )));
	}
	
	public boolean
	importKeys(
		String	str )
	
		throws CryptoManagerException
	{
		String	reason = "Key import";
	
		byte[]	existing_id 			= manager.getSecureID();
		byte[]	existing_public_key		= peekPublicKey();
		byte[]	existing_private_key	= existing_public_key==null?null:getEncryptedPrivateKey( reason );
		
		byte[]		recovered_id 			= null;
		byte[]		recovered_public_key 	= null;
		byte[]		recovered_private_key 	= null;
				
		String[]	bits = str.split( "\n" );
		
		for (int i=0;i<bits.length;i++){
			
			String	bit = bits[i].trim();
			
			if ( bit.length() == 0 ){
				
				continue;
			}
			
			String[] x = bit.split(":");
			
			if ( x.length != 2 ){
				
				continue;
			}
			
			String	lhs = x[0].trim();
			String	rhs = x[1].trim();
			
			byte[]	rhs_val = Base32.decode( rhs );
			
			if ( lhs.equals( "id" )){
				
				recovered_id = rhs_val;
				
			}else if ( lhs.equals( "public" )){
				
				recovered_public_key = rhs_val;
				
			}else if ( lhs.equals( "private" )){
				
				recovered_private_key = rhs_val;
			}
		}
		
		if ( recovered_id == null || recovered_public_key == null || recovered_private_key == null ){
			
			throw( new CryptoManagerException( "Invalid input file" ));
		}
		
		boolean	ok = false;
		
		boolean	result = false;
		
		try{
			
			result = !Arrays.equals( existing_id, recovered_id );
			
			if ( result ){
			
				manager.setSecureID( recovered_id );
			}
			
			recoverKeys( recovered_public_key, recovered_private_key );
					
			if ( !checkKeysOK( reason )){
			
				throw( new CryptoManagerException( "Invalid key pair" ));
			}
			
			ok = true;
						
		}finally{
			
			if ( !ok ){
			
				result = false;
				
				manager.setSecureID( existing_id );
				
				if ( existing_public_key != null ){
					
					recoverKeys( existing_public_key, existing_private_key );
				}
			}
		}
		
		return( result );
	}
	
	protected int
	getCurrentPasswordType()
	{
		return((int)COConfigurationManager.getIntParameter( CONFIG_PREFIX + "pwtype", CryptoManagerPasswordHandler.HANDLER_TYPE_USER ));
	}
	
	class InternalECIES 
		extends JCEIESCipher.ECIES
	{
			// we use this class to obtain compatability with BC

		public void 
		internalEngineInit(
			int                     opmode,
			Key                     key,
			AlgorithmParameterSpec  params,
			SecureRandom            random ) 
		
			throws InvalidKeyException, InvalidAlgorithmParameterException
		{
			engineInit(opmode, key, params, random);
		}

		protected byte[] 
		internalEngineDoFinal(
			byte[]  input,
			int     inputOffset,
			int     inputLen ) 
		
			throws IllegalBlockSizeException, BadPaddingException
		{
			return engineDoFinal(input, inputOffset, inputLen);
		}
	}
}
