/* 
 * Copyright 2015 by AVM GmbH <info@avm.de>
 *
 * This software contains free software; you can redistribute it and/or modify 
 * it under the terms of the GNU General Public License ("License") as 
 * published by the Free Software Foundation  (version 3 of the License). 
 * This software 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 copy of the 
 * License you received along with this software for more details.
 */

package de.avm.android.tr064.net;

import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import de.avm.android.tr064.Tr064Log;
import de.avm.android.tr064.exceptions.SslCertificateException;

/**
 * Trust manager for accepting trusted certificates
 * and check validity of not trusted certificates
 */
public class SoapTrustManager implements X509TrustManager
{
	private static final String TAG = "SoapTrustManager";
	
	private X509TrustManager mDefaultTrustManager = null;
	private IFingerprintPinningStore mPinningStore;
	
	/**
	 * @param pinningStore
	 * 		provides pinning config
	 */
	public SoapTrustManager(final IFingerprintPinningStore pinningStore)
    {
		try
		{
    		mDefaultTrustManager = getTrustManager(null);
		}
		catch(Exception e)
		{
			Tr064Log.w(TAG, "Failed to get trust manager for checking " +
                    "with CAs known by Android.", e);
		}
		
        // certificate pinning within this app
		mPinningStore = pinningStore;
    }
	
	private X509TrustManager getTrustManager(final KeyStore keyStore)
			throws NoSuchAlgorithmException, KeyStoreException
	{
		TrustManagerFactory factory = TrustManagerFactory.getInstance(
				TrustManagerFactory.getDefaultAlgorithm());
        factory.init(keyStore);
        TrustManager[] managers = factory.getTrustManagers();
        for (TrustManager tm : managers)
        	if (X509TrustManager.class.isAssignableFrom(tm.getClass()))
        		return (X509TrustManager)tm;
    	throw new NoSuchAlgorithmException("No X509TrustManager for " +
    			TrustManagerFactory.getDefaultAlgorithm());
	}

	public void checkClientTrusted(X509Certificate[] chain, String authType)
			throws CertificateException
	{
//		Log.d(TAG, "checkClientTrusted() - authType == " + authType);
		mDefaultTrustManager.checkClientTrusted(chain, authType);
	}

	public void checkServerTrusted(X509Certificate[] chain, String authType)
			throws CertificateException
	{
		Tr064Log.d(TAG, "checkServerTrusted() - authType == " + authType);
//		try
//		{
//			if (chain != null)
//			{
//				for (int ii = 0; ii < chain.length; ii++)
//					Log.d(TAG, "  chain[" + ii + "] SHA-1: \"" +
//							new Fingerprint(chain[ii], Fingerprint.Type.SHA1)
//									.toUserfriendlyString() + "\"");
//			}
//		}
//		catch(Exception e) { }
		
		// trusted by Android?
		CertificateException certExp = null;
		if (mDefaultTrustManager != null)
		{
			try
			{
				mDefaultTrustManager.checkServerTrusted(chain, authType);
//	    		Log.d(TAG, "checkServerTrusted: trusted by Android");
	        	return;
			}
			catch(CertificateException e)
			{
				certExp = e;
			}
			catch(Throwable e)
			{
				certExp = new CertificateException(
						"Failed to check certificate.", e);
			}
		}
		if ((chain == null) || (chain.length < 1))
		{
//    		Log.d(TAG, "checkServerTrusted: not trusted by Android or failed to check certificate (" +
//    				certExp.getClass().getName() + ")");
			throw (certExp != null) ? certExp :
				new CertificateException("No certificate chain to check with.");
		}

		if (mPinningStore != null)
		{
			// pinned?
			Fingerprint pubkeyFp = mPinningStore.getTrustedPublicKeyFingerprint();
			if ((pubkeyFp != null) &&
					pubkeyFp.equals(new Fingerprint(chain[0].getPublicKey(),
							pubkeyFp.getType())))
			{
//		    	Log.d(TAG, "checkServerTrusted: pinned certificate trusted");
				Fingerprint certFp = mPinningStore.getCertificateFingerprint();
				if ((certFp == null) ||
						!certFp.equals(new Fingerprint(chain[0], certFp.getType())))
				{
					mPinningStore.setCertificateFingerprint(new Fingerprint(chain[0],
							Fingerprint.Type.SHA1));
				}
				return;
			}
		}
		
		try
		{
       	 	// not trusted by trustworthy CA and not pinned
			// -> must be valid at least
			chain[0].checkValidity();				// certificate valid?
		}
		catch(CertificateException e)
		{
//    		Log.d(TAG, "checkServerTrusted: invalid (" +
//    				e.getClass().getName() + ")");
			throw e; 
		}
		catch(Throwable e)
		{
//    		Log.d(TAG, "checkServerTrusted: Failed to validate or verify certificate (" +
//    				e.getClass().getName() + ")");
			throw new CertificateException("Failed to validate or verify certificate.", e); 
		}

		if (mPinningStore != null)
		{
			if (mPinningStore.checkTrustCertificates())
			{
				// deliver not trusted chain with exception to request user consent
//				Log.d(TAG, "checkServerTrusted: not trusted but valid");
				throw new SslCertificateException(chain, certExp);
			}
//			Log.d(TAG, "checkServerTrusted: valid");
			Fingerprint certFp = mPinningStore.getCertificateFingerprint();
			if ((certFp == null) ||
					!certFp.equals(new Fingerprint(chain[0], certFp.getType())))
			{
				mPinningStore.setCertificateFingerprint(new Fingerprint(chain[0],
						Fingerprint.Type.SHA1));
			}
		}
	}
	
	public X509Certificate[] getAcceptedIssuers()
	{
		if (mDefaultTrustManager != null)
			return mDefaultTrustManager.getAcceptedIssuers();
		return null;
	}
}
