/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.encryption;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import javax.transaction.NotSupportedException;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;

import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.GUID;
import org.apache.commons.codec.binary.Base64;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;

/**
 * 
 * @since 4.0
 *
 */
public class KeyStoreTests
{
    private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();

	private TransactionService transactionService;
	private KeyStoreChecker keyStoreChecker;
	private EncryptionKeysRegistry encryptionKeysRegistry;
	private UserTransaction txn = null;
	private KeyResourceLoader keyResourceLoader;
	private List<String> toDelete;
	private DefaultEncryptor backupEncryptor;
	
    @Before
	public void setup() throws SystemException, NotSupportedException
	{
    	transactionService = (TransactionService)ctx.getBean("transactionService");
    	keyStoreChecker = (KeyStoreChecker)ctx.getBean("keyStoreChecker");
    	encryptionKeysRegistry = (EncryptionKeysRegistry)ctx.getBean("encryptionKeysRegistry");
    	keyResourceLoader = (KeyResourceLoader)ctx.getBean("springKeyResourceLoader");
        backupEncryptor = (DefaultEncryptor)ctx.getBean("backupEncryptor");

    	toDelete = new ArrayList<String>(10);

        AuthenticationUtil.setRunAsUserSystem();
		UserTransaction txn = transactionService.getUserTransaction();
		txn.begin();
	}

    @After
	public void teardown() throws IllegalStateException, SecurityException, SystemException
	{
		if(txn != null)
		{
			txn.rollback();
		}

		for(String guid : toDelete)
		{
			File file = new File(guid);
			if(file.exists())
			{
				file.delete();
			}
		}
	}

    public String generateEncodedKey()
    {
		try
		{
	    	return Base64.encodeBase64String(generateKeyData());
		}
		catch(Throwable e)
		{
			fail("Unexpected exception: " + e.getMessage());
			return null;
		}
    }

	public byte[] generateKeyData() throws NoSuchAlgorithmException
	{
		SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
		random.setSeed(System.currentTimeMillis());
		byte bytes[] = new byte[DESedeKeySpec.DES_EDE_KEY_LEN];
		random.nextBytes(bytes);
		return bytes;
	}

	protected String generateKeystoreName()
	{
		String guid = GUID.generate();	
		toDelete.add(guid);
		return guid;
	}

	protected Key generateSecretKey(String keyAlgorithm)
	{
		try
		{
			DESedeKeySpec keySpec = new DESedeKeySpec(generateKeyData());
			SecretKeyFactory kf = SecretKeyFactory.getInstance(keyAlgorithm);
	    	SecretKey secretKey = kf.generateSecret(keySpec);
	    	return secretKey;
		}
		catch(Throwable e)
		{
			fail("Unexpected exception: " + e.getMessage());
			return null;
		}
	}

	protected TestAlfrescoKeyStore getKeyStore(String name, String type, final Map<String, String> passwords, final Map<String, String> encodedKeyData,
			String keyStoreLocation, String backupKeyStoreLocation)
	{
		KeyResourceLoader testKeyResourceLoader = new KeyResourceLoader()
		{
			@Override
			public InputStream getKeyStore(String keyStoreLocation)
					throws FileNotFoundException
			{
				return keyResourceLoader.getKeyStore(keyStoreLocation);
			}

			@Override
			public Properties loadKeyMetaData(String keyMetaDataFileLocation)
					throws IOException, FileNotFoundException
			{
				Properties p = new Properties();
				p.put("keystore.password", "password");
				StringBuilder aliases = new StringBuilder();
				for(String keyAlias : passwords.keySet())
				{
					p.put(keyAlias + ".password", passwords.get(keyAlias));
					if(encodedKeyData != null && encodedKeyData.get(keyAlias) != null)
					{
						p.put(keyAlias + ".keyData", encodedKeyData.get(keyAlias));
					}
					p.put(keyAlias + ".algorithm", "DESede");
					aliases.append(keyAlias);
					aliases.append(",");
				}
				if(aliases.length() > 0)
				{
					// remove trailing comma
					aliases.delete(aliases.length() - 1, aliases.length());
				}
				p.put("aliases", aliases.toString());
				return p;
			}
		};
		
		KeyStoreParameters keyStoreParameters = new KeyStoreParameters(name, type, null, "", keyStoreLocation);
		KeyStoreParameters backupKeyStoreParameters = new KeyStoreParameters(name + ".backup", type, null, "", backupKeyStoreLocation);
		TestAlfrescoKeyStore keyStore = new TestAlfrescoKeyStore();
		keyStore.setKeyStoreParameters(keyStoreParameters);
		keyStore.setBackupKeyStoreParameters(backupKeyStoreParameters);
		keyStore.setKeyResourceLoader(testKeyResourceLoader);
		keyStore.setValidateKeyChanges(true);
		keyStore.setEncryptionKeysRegistry(encryptionKeysRegistry);
		return keyStore;
	}
	
	@Test
	public void test1()
	{
		// missing keystore, missing backup keystore, no registered keys -> create key store with metadata key and register key

		TestAlfrescoKeyStore missingMainKeyStore = getKeyStore("main", "JCEKS", Collections.singletonMap(KeyProvider.ALIAS_METADATA, "metadata"),
				Collections.singletonMap(KeyProvider.ALIAS_METADATA, generateEncodedKey()), generateKeystoreName(), generateKeystoreName());

		encryptionKeysRegistry.unregisterKey(KeyProvider.ALIAS_METADATA);
		keyStoreChecker.setMainKeyStore(missingMainKeyStore);

		try
		{
			keyStoreChecker.validateKeyStores();
		}
		catch(InvalidKeystoreException e)
		{
			fail("Unexpected exception: " + e.getMessage());
		}
		catch (MissingKeyException e)
		{
			fail("Unexpected exception : " + e.getMessage());
		}

		assertTrue("", encryptionKeysRegistry.getRegisteredKeys(missingMainKeyStore.getKeyAliases()).contains(KeyProvider.ALIAS_METADATA));
		assertTrue("", missingMainKeyStore.exists());
		assertTrue("", missingMainKeyStore.getKey(KeyProvider.ALIAS_METADATA) != null);
	}
	
	@Test
	public void test2()
	{
		// missing main keystore, missing backup keystore, metadata registered key -> error, re-instate the keystore
		TestAlfrescoKeyStore missingMainKeyStore = getKeyStore("main", "JCEKS", Collections.singletonMap(KeyProvider.ALIAS_METADATA, "metadata"),
				null, generateKeystoreName(), generateKeystoreName());
		
		assertTrue("", encryptionKeysRegistry.isKeyRegistered("metadata"));

		keyStoreChecker.setMainKeyStore(missingMainKeyStore);

		try
		{
			keyStoreChecker.validateKeyStores();
			fail("Should have caught missing main keystore");
		}
		catch(InvalidKeystoreException e)
		{
			fail("Unexpected exception : " + e.getMessage());
		}
		catch (MissingKeyException e)
		{
			// ok, expected
		}
	}

	@Test
	public void test3()
	{
		// main keystore exists, no registered metadata key -> register key
		
		// create main keystore
		TestAlfrescoKeyStore mainKeyStore = getKeyStore("main", "JCEKS", Collections.singletonMap(KeyProvider.ALIAS_METADATA, "metadata"),
				null, generateKeystoreName(), generateKeystoreName());
		createAndPopulateKeyStore(mainKeyStore);

		// de-register metadata key
		encryptionKeysRegistry.unregisterKey(KeyProvider.ALIAS_METADATA);

		// check keys
		keyStoreChecker.setMainKeyStore(mainKeyStore);

		try
		{
			keyStoreChecker.validateKeyStores();
		}
		catch(InvalidKeystoreException e)
		{
			fail("Unexpected exception: " + e.getMessage());
		}
		catch (MissingKeyException e)
		{
			fail("Unexpected exception : " + e.getMessage());
		}
		
		assertTrue("", encryptionKeysRegistry.isKeyRegistered(KeyProvider.ALIAS_METADATA));
	}

	@Test
	public void test4()
	{
		// create keystore, change key -> check for exception InvalidKey

		// Firstly, create main keystore to register a well-known key

		encryptionKeysRegistry.unregisterKey(KeyProvider.ALIAS_METADATA);

		TestAlfrescoKeyStore keyStore = getKeyStore("main", "JCEKS", Collections.singletonMap(KeyProvider.ALIAS_METADATA, "metadata"),
				null, generateKeystoreName(), generateKeystoreName());
		createAndPopulateKeyStore(keyStore);

		keyStoreChecker.setMainKeyStore(keyStore);

		// check keys
		try
		{
			// should register the metadata key
			keyStoreChecker.validateKeyStores();
		}
		catch(InvalidKeystoreException e)
		{
			fail("Unexpected exception: " + e.getMessage());
		}
		catch (MissingKeyException e)
		{
			fail("Unexpected exception : " + e.getMessage());
		}
		
		// check that the metadata key has been registered
		assertTrue("", encryptionKeysRegistry.isKeyRegistered("metadata"));

		// a changed main keystore with a different metadata key
//		TestAlfrescoKeyStore changedMainKeyStore = getKeyStore("main", "JCEKS", Collections.singletonMap(KeyProvider.ALIAS_METADATA, "metadata"),
//				null, generateKeystoreName(), generateKeystoreName());
//		createAndPopulateKeyStore(changedMainKeyStore);
		keyStore.changeKey(KeyProvider.ALIAS_METADATA, generateSecretKey("DESede"));
		
//		keyStoreChecker.setMainKeyStore(changedMainKeyStore);
		try
		{
			keyStoreChecker.validateKeyStores();
			fail("Expected key store checker to detect changed metadata key");
		}
		catch(InvalidKeystoreException e)
		{
			// ok, expected
		}
		catch (MissingKeyException e)
		{
			fail("Unexpected exception : " + e.getMessage());
		}
	}

	@Test
	public void test5()
	{
		// create main keystore, backup main keystore, change main keystore -> check that backup keystore key is ok, re-register new main key
		// check that the new main keystore key has been re-registered

		// Firstly, re-install main keystore to register a well-known key
		encryptionKeysRegistry.unregisterKey(KeyProvider.ALIAS_METADATA);

		TestAlfrescoKeyStore keyStore = getKeyStore("main", "JCEKS", Collections.singletonMap(KeyProvider.ALIAS_METADATA, "metadata"),
				null, generateKeystoreName(), generateKeystoreName());
		createAndPopulateKeyStore(keyStore);

		try
		{
			keyStoreChecker.setMainKeyStore(keyStore);
			keyStoreChecker.validateKeyStores();
		}
		catch(InvalidKeystoreException e)
		{
			fail("Unexpected exception: " + e.getMessage());
		}
		catch (MissingKeyException e)
		{
			fail("Unexpected exception : " + e.getMessage());
		}

		keyStore.backup();
		
		// check that the metadata key has been registered
		assertTrue("", encryptionKeysRegistry.isKeyRegistered("metadata"));

		// change the metadata key
		keyStore.changeKey(KeyProvider.ALIAS_METADATA, generateSecretKey("DESede"));

		try
		{
			// should detect changed metadata key and re-register it
			keyStoreChecker.validateKeyStores();
		}
		catch(InvalidKeystoreException e)
		{
			fail("Unexpected exception: " + e.getMessage());
		}
		catch (MissingKeyException e)
		{
			fail("Unexpected exception : " + e.getMessage());
		}

		// check that the new metadata key has been successfully re-registered by encrypting and decrypting some content with it
		assertTrue("", EncryptionKeysRegistry.KEY_STATUS.OK == encryptionKeysRegistry.checkKey(KeyProvider.ALIAS_METADATA,
				keyStore.getKey(KeyProvider.ALIAS_METADATA)));
	}
	
	private void createAndPopulateKeyStore(TestAlfrescoKeyStore keyStore)
	{
		KeyMap keyMap = new KeyMap();
		keyMap.setKey(KeyProvider.ALIAS_METADATA, generateSecretKey("DESede"));
		keyStore.create(keyMap, null);	
	}
	
	private static class TestAlfrescoKeyStore extends AlfrescoKeyStoreImpl
	{
		public void create(KeyMap keys, KeyMap backupKeys)
		{
			this.keys = (keys != null ? keys : new KeyMap());
			this.backupKeys = (backupKeys != null ? backupKeys : new KeyMap());
			super.create();
		}
		
		void changeKey(String keyAlias, Key key)
		{
			keys.setKey(KeyProvider.ALIAS_METADATA, key);
		}
	}
}
