/**
 * OWASP Enterprise Security API (ESAPI)
 *
 * This file is part of the Open Web Application Security Project (OWASP)
 * Enterprise Security API (ESAPI) project. For details, please see
 * <a href="http://www.owasp.org/index.php/ESAPI">http://www.owasp.org/index.php/ESAPI</a>.
 *
 * Copyright (c) 2007 - The OWASP Foundation
 *
 * The ESAPI is published by OWASP under the BSD license. You should read and accept the
 * LICENSE before you use, modify, and/or redistribute this software.
 *
 * @author Jeff Williams <a href="http://www.aspectsecurity.com">Aspect Security</a>
 * @created 2007
 */
package org.owasp.esapi.reference.crypto;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Properties;
import java.util.Set;

import org.owasp.esapi.ESAPI;
import org.owasp.esapi.EncryptedProperties;
import org.owasp.esapi.Logger;
import org.owasp.esapi.crypto.CipherText;
import org.owasp.esapi.crypto.PlainText;
import org.owasp.esapi.errors.EncryptionRuntimeException;

/**
 * Reference implementation of the {@code EncryptedProperties} interface. This
 * implementation wraps a normal properties file, and creates surrogates for the
 * {@code getProperty} and {@code setProperty} methods that perform encryption
 * and decryption based on {@code Encryptor}.
 * <p>
 * This implementation differs from {@code DefaultEncryptedProperties} in that
 * it actually extends from {@code java.util.Properties} for applications that need an
 * instance of that class. In order to do so, the {@code getProperty} and
 * {@code setProperty} methods were modified to throw {@code EncryptionRuntimeException}
 * instead of {@code EncryptionException}.
 *
 * @author August Detlefsen (augustd at codemagi dot com)
 *         <a href="http://www.codemagi.com">CodeMagi, Inc.</a>
 * @author kevin.w.wall@gmail.com
 * @since October 8, 2010
 * @see org.owasp.esapi.EncryptedProperties
 * @see org.owasp.esapi.reference.crypto.DefaultEncryptedProperties
 */
public class ReferenceEncryptedProperties extends java.util.Properties implements EncryptedProperties {

	/**
	 * serverVersionUID; use format of YYYYMMDD.
	 */
	private static final long serialVersionUID = 20120718L;

	/** The logger. */
	private final Logger logger = ESAPI.getLogger(this.getClass());

	private static final String[] GET_ERROR_MESSAGES = new String[]{
		": failed decoding from base64",
		": failed to deserialize properly",
		": failed to decrypt properly"
	};

	private static final String[] SET_ERROR_MESSAGES = new String[]{
		": failed to encrypt properly",
		": failed to serialize correctly",
		": failed to base64-encode properly",
		": failed to set base64-encoded value as property. Illegal key name?"
	};

	/**
	 * Instantiates a new encrypted properties.
	 */
	public ReferenceEncryptedProperties() {
		super();
	}

	public ReferenceEncryptedProperties(Properties defaults) {
		super();

		for (Object oKey : defaults.keySet()) {
			String key		= (oKey instanceof String) ? (String)oKey : oKey.toString();
			String value	= defaults.getProperty(key);

			this.setProperty(key, value);
		}
	}

	/**
	 * {@inheritDoc}
	 *
	 * @throws EncryptionRuntimeException Thrown if decryption fails.
	 */
	@Override
	public synchronized String getProperty(String key) throws EncryptionRuntimeException {
	    int progressMark = 0;
	    try {
	        String encryptedValue = super.getProperty(key);

	        if(encryptedValue==null)
	            return null;

	        progressMark = 0;
	        byte[] serializedCiphertext   = ESAPI.encoder().decodeFromBase64(encryptedValue);
	        progressMark++;
	        CipherText restoredCipherText = CipherText.fromPortableSerializedBytes(serializedCiphertext);
	        progressMark++;
	        PlainText plaintext           = ESAPI.encryptor().decrypt(restoredCipherText);

	        return plaintext.toString();
		} catch (Exception e) {
			throw new EncryptionRuntimeException("Property retrieval failure",
					                             "Couldn't retrieve encrypted property for property " + key +
												 GET_ERROR_MESSAGES[progressMark], e);
	    }
	}

	/**
	 * {@inheritDoc}
	 *
	 * @throws EncryptionRuntimeException Thrown if decryption fails.
	 */
	@Override
	public synchronized String getProperty(String key, String defaultValue) throws EncryptionRuntimeException {
		String value = getProperty(key);

		if (value == null) return defaultValue;

		return value;
	}

	/**
	 * {@inheritDoc}
	 *
	 * @throws EncryptionRuntimeException Thrown if encryption fails.
	 */
	@Override
	public synchronized String setProperty(String key, String value) throws EncryptionRuntimeException {
	    int progressMark = 0;
	    try {
	        if ( key == null ) {
	            throw new NullPointerException("Property name may not be null.");
	        }
	        if ( value == null ) {
	            throw new NullPointerException("Property value may not be null.");
	        }
	        // NOTE: Not backward compatible w/ ESAPI 1.4.
	        PlainText pt = new PlainText(value);
	        CipherText ct = ESAPI.encryptor().encrypt(pt);
	        progressMark++;
	        byte[] serializedCiphertext = ct.asPortableSerializedByteArray();
	        progressMark++;
	        String b64str = ESAPI.encoder().encodeForBase64(serializedCiphertext, false);
	        progressMark++;
	        return (String)super.put(key, b64str);
	    } catch (Exception e) {
	        throw new EncryptionRuntimeException("Property setting failure",
	                                      "Couldn't set encrypted property " + key +
	                                      SET_ERROR_MESSAGES[progressMark], e);
	    }
	}

	/**
	 * {@inheritDoc}
	 * @throws IOException Thrown if input stream invalid or does not
	 * 					   correspond to Java properties file format.
	 */
	@Override
	public void load(InputStream in) throws IOException {
		super.load(in);
		logger.trace(Logger.SECURITY_SUCCESS, "Encrypted properties loaded successfully");
	}

	/**
	 * {@inheritDoc}
	 *
	 * For JDK 1.5 compatibility, this method has been overridden convert the Reader
	 * into an InputStream and call the superclass constructor.
	 * 
	 * @throws IOException Thrown if {@code Reader} input stream invalid or does not
	 * 					   correspond to Java properties file format.
	 */
	public void load(Reader in) throws IOException {

		if (in == null) return;

		//read from the reader into a StringBuffer
		char[] cbuf				= new char[65536];
		BufferedReader buff		= new BufferedReader(in);
		StringBuilder contents	= new StringBuilder();

		int read_this_time = 0;
		while (read_this_time != -1) {
			read_this_time = buff.read(cbuf, 0, 65536);
			if (read_this_time > 0) contents.append(cbuf, 0, read_this_time);
		}

		//create a new InputStream from the StringBuffer
		InputStream is = new ByteArrayInputStream(contents.toString().getBytes());

		super.load(is);
		logger.trace(Logger.SECURITY_SUCCESS, "Encrypted properties loaded successfully");
	}

	/**
	 * This method has been overridden to throw an {@code UnsupportedOperationException}
	 */
	@Override
	public void list(PrintStream out) {
		throw new UnsupportedOperationException("This method has been removed for security.");
	}

	/**
	 * This method has been overridden to throw an {@code UnsupportedOperationException}
	 */
	@Override
	public void list(PrintWriter out) {
		throw new UnsupportedOperationException("This method has been removed for security.");
	}

	/**
	 * This method has been overridden to throw an {@code UnsupportedOperationException}
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	@Override
	public Collection values() {
		throw new UnsupportedOperationException("This method has been removed for security.");
	}

	/**
	 * This method has been overridden to throw an {@code UnsupportedOperationException}
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	@Override
	public Set entrySet() {
		throw new UnsupportedOperationException("This method has been removed for security.");
	}

	/**
	 * This method has been overridden to throw an {@code UnsupportedOperationException}
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	@Override
	public Enumeration elements() {
		throw new UnsupportedOperationException("This method has been removed for security.");
	}

	/**
	 * This method has been overridden to only accept Strings for key and value, and to encrypt
	 * those Strings before storing them. Outside classes should always use {@code setProperty}
	 * to add values to the Properties map. If an outside class does erroneously call this method 
	 * with non-String parameters an {@code IllegalArgumentException} will be thrown.
	 *
	 * @param key	A String key to add
	 * @param value A String value to add
	 * @return		The old value associated with the specified key, or {@code null}
     *				if the key did not exist.
	 */
	@Override
	public synchronized Object put(Object key, Object value) {
		//if java.util.Properties is calling this method, just forward to the implementation in
		//the superclass (java.util.Hashtable)
		Throwable t = new Throwable();
		for (StackTraceElement trace : t.getStackTrace()) {
			if ("java.util.Properties".equals(trace.getClassName()) ) return super.put(key, value);
		}

		//otherwise, if both arguments are Strings, encrypt and store them
		if (key instanceof String && value instanceof String) return setProperty((String)key, (String)value);

		//other Object types are not allowed
		throw new IllegalArgumentException("This method has been overridden to only accept Strings for key and value.");
	}

	/**
	 * This method has been overridden to not print out the keys and values stored in this properties file.
	 *
	 * @return The minimal String representation of this class, as per java.lang.Object.
	 */
	@Override
	public String toString() {
		return getClass().getName() + "@" + Integer.toHexString(hashCode());
	}

}
