/*
 * 
 * Copyright 2004 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * The contents of this file are subject to the Sun Community Source
 * License v3.0/Jini Technology Specific Attachment v 1.0 (the
 * "License"). You may not use this file except in compliance with the
 * License. You may obtain a copy of the License at
 * http://wwws.sun.com/software/jini/licensing/SCSL3_JiniTSA1.html.
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Reference Code is Jini Technology Core Platform code, v2.0. The
 * Developer of the Reference Code is Sun Microsystems, Inc.
 * 
 * Contributor(s): Sun Microsystems, Inc.
 * 
 * The contents of this file comply with the Jini Technology Core
 * Platform Compatibility Kit, v2.0A.
 * 
 * Tester(s): Sun Microsystems, Inc.
 * 
 * Test Platform(s):
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Solaris(TM)SPARC/x86
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Linux (Intel x86)
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Windows (Intel Platform)
 * 
 */

package net.jini.jeri.ssl;

import com.sun.jini.action.GetPropertyAction;
import com.sun.jini.collection.WeakSoftTable;
import java.lang.ref.ReferenceQueue;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.cert.CertPath;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.security.auth.AuthPermission;
import javax.security.auth.Subject;
import javax.security.auth.x500.X500Principal;
import net.jini.core.constraint.ClientMaxPrincipal;
import net.jini.core.constraint.ClientMinPrincipal;
import net.jini.core.constraint.ConstraintAlternatives;
import net.jini.core.constraint.Integrity;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.ServerMinPrincipal;
import net.jini.io.UnsupportedConstraintException;
import net.jini.jeri.Endpoint;
import net.jini.security.Security;

/**
 * Provides miscellaneous utilities for the classes in this package.
 *
 * @author Sun Microsystems, Inc.
 */
abstract class Utilities {

    /* -- Fields -- */

    /**
     * The names of JSSE key exchange algorithms used for anonymous
     * communication.
     */
    private static final String[] ANONYMOUS_KEY_EXCHANGE_ALGORITHMS = {
	"DH_anon",
	"DH_anon_EXPORT"
    };

    /** The names of JSSE key exchange algorithms that use RSA keys. */
    private static final String[] RSA_KEY_EXCHANGE_ALGORITHMS = {
	"DHE_RSA",
	"DHE_RSA_EXPORT",
	"DH_RSA",
	"DH_RSA_EXPORT",
	"RSA",
	"RSA_EXPORT"
    };

    /** The names of JSSE key exchange algorithms that use DSA keys. */
    private static final String[] DSA_KEY_EXCHANGE_ALGORITHMS = {
	"DHE_DSS",
	"DHE_DSS_EXPORT",
	"DH_DSS",
	"DH_DSS_EXPORT"
    };

    /**
     * The names of all the JSSE key exchange algorithms supported by this
     * provider.
     */
    private static final String[] SUPPORTED_KEY_EXCHANGE_ALGORITHMS = {
	"DH_anon",
	"DH_anon_EXPORT",
	"DHE_RSA",
	"DHE_RSA_EXPORT",
	"DH_RSA",
	"DH_RSA_EXPORT",
	"RSA",
	"RSA_EXPORT",
	"DHE_DSS",
	"DHE_DSS_EXPORT",
	"DH_DSS",
	"DH_DSS_EXPORT"
    };	

    /**
     * The name of the JSSE message integrity code algorithm that does not
     * insure integrity.
     */
    private static final String NO_INTEGRITY_MIC_ALGORITHM = "NULL";

    /** The name of the JSSE cipher algorithm that provides no encryption */
    private static final String NO_ENCRYPTION_CIPHER_ALGORITHM = "NULL";

    /** The names of cipher algorithms that do strong encryption */
    private static final String[] STRONG_ENCRYPTION_CIPHERS = {
	"3DES_EDE_CBC",
	"AES_128_CBC",
	"AES_256_CBC",
	"IDEA_CBC",
	"RC4_128"
    };

    /** The names of all cipher algorithms supported by this provider. */
    private static final String[] SUPPORTED_ENCRYPTION_CIPHERS = {
	"3DES_EDE_CBC",
	"AES_128_CBC",
	"AES_256_CBC",
	"DES40_CBC",
	"DES_CBC",
	"IDEA_CBC",
	"NULL",
	"RC2_CBC_40",
	"RC4_128",
	"RC4_40"
    };

    /** Client logger */
    static final Logger clientLogger =
	Logger.getLogger("net.jini.jeri.ssl.client");

    /** Server logger */
    static final Logger serverLogger =
	Logger.getLogger("net.jini.jeri.ssl.server");

    /** Initialization logger */
    static final Logger initLogger =
	Logger.getLogger("net.jini.jeri.ssl.init");

    /**
     * Returned by getPermittedKeyAlgorithms when any key algorithm is
     * permitted.
     */
    static final int ANY_KEY_ALGORITHM = 0xffffffff;

    /**
     * Or'ed into the value returned by getPermittedKeyAlgorithms when DSA keys
     * are permitted.
     */
    static final int DSA_KEY_ALGORITHM = 1 << 0;

    /**
     * Or'ed into the value returned by getPermittedKeyAlgorithms when RSA keys
     * are permitted.
     */
    static final int RSA_KEY_ALGORITHM = 1 << 1;

    /** Stores SSL contexts and auth managers. */
    private static final WeakSoftTable sslContextMap = new WeakSoftTable();

    /**
     * The cipher suites supported by the JSSE implementation, or null if not
     * set yet.
     */
    private static String[] supportedCipherSuitesInternal = null;

    /**
     * The cipher suites specified by the user, or null if not specified.  The
     * field is not final to aid testing.
     */
    private static String[] requestedCipherSuites;
    static {
	String value = (String) Security.doPrivileged(
	    new GetPropertyAction("com.sun.jini.jeri.ssl.cipherSuites"));
	if (value == null) {
	    requestedCipherSuites = null;
	} else {
	    StringTokenizer tokens = new StringTokenizer(value, ",");
	    final int count = tokens.countTokens();
	    requestedCipherSuites = new String[count];
	    for (int i = 0; i < count; i++) {
		requestedCipherSuites[i] = tokens.nextToken();
	    }
	}
    }

    /** An X.509 certificate factory for creating CertPaths. */
    private static CertificateFactory certFactory;

    /**
     * Represents a principal whose name is not known.  Used, for example, to
     * represent the server principal when the server subject is not available.
     */
    static final Principal UNKNOWN_PRINCIPAL = new Principal() {
	public String toString() { return "UNKNOWN_PRINCIPAL"; }
	public String getName() { return toString(); }
    };

    /** Constraints that require Integrity.YES. */
    static final InvocationConstraints INTEGRITY_REQUIRED =
	new InvocationConstraints(Integrity.YES, null);

    /** Constraints that prefer Integrity.YES. */
    static final InvocationConstraints INTEGRITY_PREFERRED =
	new InvocationConstraints(null, Integrity.YES);

    /** The secure socket protocol used with JSSE. */
    private static final String sslProtocol = (String) Security.doPrivileged(
	new GetPropertyAction("com.sun.jini.jeri.ssl.sslProtocol", "TLS"));

    /** Permission needed to access the current subject. */
    static final AuthPermission getSubjectPermission =
	new AuthPermission("getSubject");

    /* -- Methods -- */

    /* -- getSupportedCipherSuites -- */

    /**
     * Returns all the cipher suites supported by the JSSE implementation and
     * this provider.
     */
    static String[] getSupportedCipherSuites() {
	synchronized (sslContextMap) {
	    if (supportedCipherSuitesInternal == null) {
		SSLContextInfo info = getServerSSLContextInfo(null, null);
		SSLSocketFactory factory = info.sslContext.getSocketFactory();
		supportedCipherSuitesInternal =
		    getSupportedCipherSuites(factory);
	    }
	    return supportedCipherSuitesInternal;
	}
    }

    /**
     * Returns all the cipher suites supported by the socket factory and this
     * provider.  Uses the requested cipher suites, if any.
     */
    private static String[] getSupportedCipherSuites(
	SSLSocketFactory factory)
    {
	String[] suites;
	if (requestedCipherSuites == null) {
	    suites = factory.getSupportedCipherSuites();
	} else if (requestedCipherSuites.length == 0) {
	    initLogger.log(Level.WARNING,
			   "Problem with requested cipher suites: " +
			   "No suites specified -- " +
			   "using default suites");
	    suites = factory.getSupportedCipherSuites();
	} else {
	    /*
	     * Set the enabled suites on a socket to make sure they are
	     * supported by JSSE.
	     */
	    try {
		SSLSocket socket = (SSLSocket) factory.createSocket();
		socket.setEnabledCipherSuites(requestedCipherSuites);
		suites = requestedCipherSuites;
	    } catch (Exception e) {
		initLogger.log(Level.WARNING,
			       "Problem with requested cipher suites -- " +
			       "using default suites",
			       e);
		suites = factory.getSupportedCipherSuites();
	    }
	}
	return getSupportedCipherSuites(suites);
    }

    /**
     * Filters out unsupported suites, modifying the argument and maintaining
     * the original order.
     */
    private static String[] getSupportedCipherSuites(String[] suites) {
	int max = suites.length;
	for (int i = suites.length; --i >= 0; ) {
	    if (!supportedCipherSuite(suites[i])) {
		if (i < max - 1) {
		    System.arraycopy(suites, i + 1, suites, i, max - i - 1);
		}
		max--;
	    }
	}
	if (max == suites.length) {
	    return suites;
	} else {
	    String[] copy = new String[max];
	    System.arraycopy(suites, 0, copy, 0, max);
	    return copy;
	}
    }

    /* -- principal constraints methods -- */

    /**
     * Returns all client principals referred to by the constraints or null if
     * no client principal constraints are specified.  Returns a new set if the
     * result is non-null.
     */
    static Set getClientPrincipals(InvocationConstraints constraints) {
	return getPrincipals(constraints, true);
    }

    /**
     * Returns all client principals referred to by the constraints or null if
     * no client principal constraints are specified.  Returns a new set if the
     * result is non-null.
     */
    static Set getClientPrincipals(Set constraints) {
	return getPrincipals(constraints, true);
    }

    /**
     * Returns all server principals referred to by the constraints or null if
     * no server principal constraints are specified.  Returns a new set if the
     * result is non-null.
     */
    static Set getServerPrincipals(InvocationConstraints constraints) {
	return getPrincipals(constraints, false);
    }

    /** Implements getClientPrincipals or getServerPrincipals. */
    private static Set getPrincipals(InvocationConstraints constraints,
				     boolean client)
    {
	Set requirements = getPrincipals(constraints.requirements(), client);
	Set preferences = getPrincipals(constraints.preferences(), client);
	if (requirements == null) {
	    return preferences;
	} else if (preferences == null) {
	    return requirements;
	} else {
	    requirements.addAll(preferences);
	    return requirements;
	}
    }

    /**
     * Returns the client or server principals referred to by a set of
     * constraints.
     */
    private static Set getPrincipals(Set constraints, boolean client) {
	Set result = null;
	for (Iterator i = constraints.iterator(); i.hasNext(); ) {
	    Set eltResult =
		getPrincipals((InvocationConstraint) i.next(), client);
	    if (eltResult != null) {
		if (result == null) {
		    result = new HashSet();
		}
		result.addAll(eltResult);
	    }
	}
	return result;
    }

    /**
     * Returns the principals specified by a ClientMinPrincipal,
     * ClientMaxPrincipal, or ServerMinPrincipal constraint, or an alternatives
     * of one of those types.
     */
    private static Set getPrincipals(InvocationConstraint constraint,
				     boolean client)
    {
	Set principals;
	if (constraint instanceof ConstraintAlternatives) {
	    Set alts = ((ConstraintAlternatives) constraint).elements();
	    return getPrincipals(alts, client);
	} else if (constraint instanceof ServerMinPrincipal) {
	    if (client) {
		return null;
	    }
	    principals = ((ServerMinPrincipal) constraint).elements();
	} else if (!client) {
	    return null;
	} else if (constraint instanceof ClientMinPrincipal) {
	    principals = ((ClientMinPrincipal) constraint).elements();
	} else if (constraint instanceof ClientMaxPrincipal) {
	    principals = ((ClientMaxPrincipal) constraint).elements();
	} else {
	    return null;
	}
	Set result = new HashSet(principals.size());
	for (Iterator i = principals.iterator(); i.hasNext(); ) {
	    Object elt = i.next();
	    if (elt instanceof X500Principal) {
		result.add(elt);
	    }
	}
	return result;
    }

    /* -- get*SSLContextInfo -- */

    /** Used to pass an SSLContext and AuthManager pair. */
    static class SSLContextInfo {
	final SSLContext sslContext;
	final AuthManager authManager;

	SSLContextInfo(SSLContext sslContext, AuthManager authManager) {
	    this.sslContext = sslContext;
	    this.authManager = authManager;
	}
    }

    /**
     * WeakKey for looking up a server SSLContext.  Stores a weak reference to
     * the subject, plus the permitted principals.
     */
    private static class ServerKey extends WeakSoftTable.WeakKey {
	final Set permittedLocalPrincipals;

	/** Creates a key for the specified subject and local principals */
	ServerKey(Subject subject, Set permittedLocalPrincipals) {
	    super(subject);
	    this.permittedLocalPrincipals = permittedLocalPrincipals;
	}

	/** Copies the key to the queue */
	ServerKey(ServerKey serverKey, ReferenceQueue queue) {
	    super(serverKey, queue);
	    permittedLocalPrincipals = serverKey.permittedLocalPrincipals;
	}

	public WeakSoftTable.RemovableReference copy(ReferenceQueue queue) {
	    return new ServerKey(this, queue);
	}

	public int hashCode() {
	    return super.hashCode()
		^ (permittedLocalPrincipals == null ? 1
		   : permittedLocalPrincipals.hashCode());
	}

	public boolean equals(Object other) {
	    return super.equals(other)
		&& safeEquals(permittedLocalPrincipals,
			      ((ServerKey) other).permittedLocalPrincipals);
	}
    }

    /**
     * WeakKey for looking up a client SSLContext.  Stores a weak reference to
     * the subject, plus the permitted client and server principals, the
     * endpoint, and whether client authentication is required.
     */
    private final static class ClientKey extends ServerKey {
	final Set permittedRemotePrincipals;
	final Endpoint endpoint;
	final boolean clientAuthRequired;
	final String[] cipherSuites;

	/** Creates a key for the specified client call context. */
	ClientKey(CallContext callContext) {
	    super(callContext.clientSubject, callContext.clientPrincipals);
	    permittedRemotePrincipals = callContext.serverPrincipals;
	    endpoint = callContext.endpoint;
	    clientAuthRequired = callContext.clientAuthRequired;
	    cipherSuites = callContext.cipherSuites;
	    assert cipherSuites != null
		&& cipherSuites.length > 0
		&& cipherSuites[0] != null;
	}

	/** Copies the key to the queue. */
	private ClientKey(ClientKey clientKey, ReferenceQueue queue) {
	    super(clientKey, queue);
	    permittedRemotePrincipals = clientKey.permittedRemotePrincipals;
	    endpoint = clientKey.endpoint;
	    clientAuthRequired = clientKey.clientAuthRequired;
	    cipherSuites = clientKey.cipherSuites;
	}

	public WeakSoftTable.RemovableReference copy(ReferenceQueue queue) {
	    return new ClientKey(this, queue);
	}

	public int hashCode() {
	    return super.hashCode()
		^ (permittedRemotePrincipals == null ? 2
		   : permittedRemotePrincipals.hashCode())
		^ endpoint.hashCode()
		^ (clientAuthRequired ? 4 : 0)
		^ cipherSuites[0].hashCode();
	}

	public boolean equals(Object other) {
	    if (!super.equals(other)) {
		return false;
	    }
	    ClientKey clientKey = (ClientKey) other;
	    return safeEquals(permittedRemotePrincipals,
			      clientKey.permittedRemotePrincipals)
		&& endpoint.equals(clientKey.endpoint)
		&& clientAuthRequired == clientKey.clientAuthRequired
		&& Arrays.equals(cipherSuites, clientKey.cipherSuites);
	}
    }

    /**
     * Used to store a soft reference to a SSLContext and the associated
     * AuthManager in the SSL context map.
     */
    private final static class Value extends WeakSoftTable.SoftValue {
	final AuthManager authManager;

	/**
	 * Creates a value for the associated key containing the specified SSL
	 * context and auth manager.
	 */
	Value(ServerKey key, SSLContext sslContext, AuthManager authManager) {
	    super(key, sslContext);
	    this.authManager = authManager;
	}

	/** Copies the value to the queue. */
	private Value(Value value, ReferenceQueue queue) {
	    super(value, queue);
	    this.authManager = value.authManager;
	}

	public WeakSoftTable.RemovableReference copy(ReferenceQueue queue) {
	    return new Value(this, queue);
	}

	/** Returns the SSL context. */
	SSLContext getSSLContext() {
	    return (SSLContext) get();
	}
    }

    /**
     * Returns the SSLContext and ClientAuthManager to use for creating client
     * socket factories.  Each client connection has exclusive access to an
     * SSLContext while the opening an SSL connection and should return the
     * SSLContextInfo by calling releaseClientSSLContextInfo when the
     * connection handshake is done, unless the handshake fails.
     *
     * @param callContext the client call context
     * @return an SSLContextInfo containing an SSLContext and ClientAuthManager
     *	       to use for a connection described by the argument
     * @throws RuntimeException if an error occurs during initialization of
     *	       JSSE
     */
    static SSLContextInfo getClientSSLContextInfo(CallContext callContext) {
	ClientKey key = new ClientKey(callContext);
	synchronized (sslContextMap) {
	    for (int i = 0; true; i++) {
		Value value = (Value) sslContextMap.get(key, i);
		if (value == null) {
		    break;
		}
		SSLContext sslContext = value.getSSLContext();
		if (sslContext != null) {
		    ClientAuthManager authManager =
			(ClientAuthManager) value.authManager;
		    try {
			authManager.checkAuthentication();
		    } catch (SecurityException e) {
			continue;
		    } catch (UnsupportedConstraintException e) {
			continue;
		    }
		    sslContextMap.remove(key, i);
		    if (clientLogger.isLoggable(Level.FINEST)) {
			clientLogger.log(
			    Level.FINEST,
			    "get client SSL context for {0}\n" +
			    "returns existing {1}",
			    new Object[] { callContext, sslContext });
		    }
		    return new SSLContextInfo(sslContext, authManager);
		}
	    }
	}
	/* Create a new SSL context */
	SSLContext sslContext;
	try {
	    sslContext = SSLContext.getInstance(sslProtocol);
	} catch (NoSuchAlgorithmException e) {
	    throw initializationError(e, "finding SSL context");
	}

	ClientAuthManager authManager;
	try {
	    authManager = new ClientAuthManager(
		callContext.clientSubject,
		callContext.clientPrincipals,
		callContext.serverPrincipals);
	} catch (NoSuchAlgorithmException e) {
	    throw initializationError(e, "creating key or trust manager");
	}

	/* Initialize SSL context */
	try {
	    sslContext.init(new KeyManager[] { authManager },
			    new TrustManager[] { authManager },
			    null);
	} catch (KeyManagementException e) {
	    throw initializationError(e, "initializing SSL context");
	}

	if (clientLogger.isLoggable(Level.FINEST)) {
	    clientLogger.log(Level.FINEST,
			     "get client SSL context for {0}\nreturns new {1}",
			     new Object[] { callContext, sslContext });
	}
	return new SSLContextInfo(sslContext, authManager);
    }

    /**
     * Returns the SSLContext and ServerAuthManager to use for creating server
     * socket factories.  Server connections with the same subject and
     * principals share the same SSLContext.
     *
     * @param serverSubject the subject, or null
     * @param serverPrincipals the permitted principals, or null
     * @return an SSLContextInfo containing an SSLContext and ServerAuthManager
     *	       to use for a connection described by the arguments
     * @throws RuntimeException if an error occurs during initialization of
     *	       JSSE
     */
    static SSLContextInfo getServerSSLContextInfo(Subject serverSubject,
						  Set serverPrincipals)
    {
	ServerKey key = new ServerKey(serverSubject, serverPrincipals);
	synchronized (sslContextMap) {
	    Value value = (Value) sslContextMap.get(key, 0);
	    if (value != null) {
		SSLContext sslContext = value.getSSLContext();
		if (sslContext != null) {
		    if (serverLogger.isLoggable(Level.FINEST)) {
			serverLogger.log(
			    Level.FINEST,
			    "get server SSL context for {0}\n" +
			    "and principals {1}\nreturns existing {2}",
			    new Object[] { subjectString(serverSubject),
					   serverPrincipals, sslContext });
		    }
		    return new SSLContextInfo(sslContext, value.authManager);
		}
	    }

	    SSLContext sslContext;
	    try {
		sslContext = SSLContext.getInstance(sslProtocol);
	    } catch (NoSuchAlgorithmException e) {
		throw initializationError(e, "finding SSL context");
	    }

	    ServerAuthManager authManager;
	    try {
		authManager = new ServerAuthManager(
		    serverSubject, serverPrincipals,
		    sslContext.getServerSessionContext());
	    } catch (NoSuchAlgorithmException e) {
		throw initializationError(e, "creating key or trust manager");
	    }

	    /* Initialize SSL context */
	    try {
		sslContext.init(new KeyManager[] { authManager },
				new TrustManager[] { authManager },
				null);
	    } catch (KeyManagementException e) {
		throw initializationError(e, "initializing SSL context");
	    }

	    sslContextMap.add(key, new Value(key, sslContext, authManager));

	    if (serverLogger.isLoggable(Level.FINEST)) {
		serverLogger.log(
		    Level.FINEST,
		    "get server SSL context for {0}\nand principals {1}\n" +
		    "returns new {2}",
		    new Object[] { subjectString(serverSubject),
				   serverPrincipals, sslContext });
	    }
	    return new SSLContextInfo(sslContext, authManager);
	}
    }

    /**
     * Returns the client's SSLContext and ClientAuthManager to the
     * SSLContextMap for use by another connection.
     */
    static void releaseClientSSLContextInfo(CallContext callContext,
					    SSLContext sslContext,
					    ClientAuthManager authManager)
    {
	ClientKey key = new ClientKey(callContext);
	synchronized (sslContextMap) {
	    sslContextMap.add(key, new Value(key, sslContext, authManager));
	}
    }

    /**
     * Returns a <code>RuntimeException</code> for a problem initializing JSSE.
     *
     * @param error an <code>Exception</code> that describes the problem
     * @param contextString describes where the problem occurred
     * @return a <code>RuntimeException</code> describing the problem
     */
    private static RuntimeException initializationError(Exception error,
							String contextString)
    {
	RuntimeException e = new RuntimeException(
	    "Error during initialization of SSL or HTTPS provider, " +
	    "while " + contextString + ": " + error.getMessage(),
	    error);
	initLogger.log(Level.WARNING, "Initialization error", e);
	return e;
    }

    /**
     * Returns a CertificateFactory for generating a CertPath for X.509
     * certificates.
     */
    static CertificateFactory getCertFactory() {
	synchronized (sslContextMap) {
	    if (certFactory == null) {
		try {
		    certFactory = CertificateFactory.getInstance("X.509");
		} catch (CertificateException e) {
		    throw initializationError(
			e, "getting certificate factory");
		}
	    }
	    return certFactory;
	}
    }

    /* -- firstX509Cert -- */

    /**
     * Returns the first X509Certificate from a CertPath known to contain them.
     */
    static X509Certificate firstX509Cert(CertPath certPath) {
 	return (X509Certificate) certPath.getCertificates().get(0);
    }

    /* -- checkValidity -- */

    /**
     * Checks if the X.509 certificates in the CertPath are currently valid.
     * If issuers is non-null, insures the path contains one of the issuers.
     */
    static void checkValidity(CertPath x509CertPath, X500Principal[] issuers)
	throws CertificateException
    {
	boolean issuerOK = issuers == null;
	List certs = x509CertPath.getCertificates();
	for (int i = certs.size(); --i >= 0; ) {
	    X509Certificate cert = (X509Certificate) certs.get(i);
	    cert.checkValidity();
	    if (!issuerOK) {
		X500Principal certIssuer = cert.getIssuerX500Principal();
		for (int j = issuers.length; --j >= 0; ) {
		    if (certIssuer.equals(issuers[j])) {
			issuerOK = true;
			break;
		    }
		}
	    }
	}
	if (!issuerOK) {
	    throw new CertificateException(
		"No match for permitted issuers: " + toString(issuers) +
		"\nCertificate chain: " + x509CertPath);
	}
    }

    /* -- Cipher suite info -- */

    /** Determines if the cipher suite authenticates the server */
    static boolean doesServerAuthentication(String cipherSuite) {
	String alg = getKeyExchangeAlgorithm(cipherSuite);
	return position(alg, ANONYMOUS_KEY_EXCHANGE_ALGORITHMS) == -1;
    }

    /** Determines if the cipher suite does encryption */
    static boolean doesEncryption(String cipherSuite) {
	return !getCipherAlgorithm(cipherSuite).equals(
	    NO_ENCRYPTION_CIPHER_ALGORITHM);
    }

    /** Determines if the cipher suite maintains integrity */
    static boolean maintainsIntegrity(String cipherSuite) {
	return !getMICAlgorithm(cipherSuite).equals(
	    NO_INTEGRITY_MIC_ALGORITHM);
    }

    /**
     * Returns the key exchange algorithm for the specified cipher suite. <p>
     *
     * The key exchange algorithm is found following the first underscore and
     * up to the first occurrence of "_WITH_".
     */
    static String getKeyExchangeAlgorithm(String cipherSuite) {
	int start = cipherSuite.indexOf('_') + 1;
	if (start >= 1) {
	    int end = cipherSuite.indexOf("_WITH_", start);
	    if (end >= start) {
		return cipherSuite.substring(start, end);
	    }
	}
	return "NULL";
    }

    /**
     * Returns the key algorithm for the specified cipher suite, one of "RSA",
     * "DSA", or "NULL".  Throws an IllegalArgumentException if the algorithm
     * is not recognized. <p>
     *
     * The key algorithm is specified by the key exchange algorithm.
     */
    static String getKeyAlgorithm(String cipherSuite) {
	String alg = getKeyExchangeAlgorithm(cipherSuite);
	if (position(alg, RSA_KEY_EXCHANGE_ALGORITHMS) != -1) {
	    return "RSA";
	} else if (position(alg, DSA_KEY_EXCHANGE_ALGORITHMS) != -1) {
	    return "DSA";
	} else if (position(alg, ANONYMOUS_KEY_EXCHANGE_ALGORITHMS) != -1) {
	    return "NULL";
	} else {
	    throw new IllegalArgumentException(
		"Unrecognized key exchange algorithm: " + alg);
	}
    }   

    /**
     * Returns the algorithms permitted for keys used with this cipher suite.
     * Note that the result can be different for client and server sides.
     *
     * @param cipherSuite the cipher suite
     * @param client true to get results for the client side, false for the
     *	      server side
     * @return the permitted key algorithms, an OR of some set of the values
     *	       DSA_KEY_ALGORITHM and RSA_KEY_ALGORITHM
     * @throws IllegalArgumentException if the key exchange algorithm is not
     *	       recognized
     */
    static int getPermittedKeyAlgorithms(String cipherSuite, boolean client) {
	String keyAlgorithm = getKeyAlgorithm(cipherSuite);
	if (keyAlgorithm.equals("RSA")) {
	    /*
	     * For these suites, the server must use an RSA key, but the client
	     * may use either an RSA or DSA key.
	     */
	    return (client)
		? DSA_KEY_ALGORITHM | RSA_KEY_ALGORITHM
		: RSA_KEY_ALGORITHM;
	} else if (keyAlgorithm.equals("DSA")) {
	    /* Same here, but server must use a DSA key */
	    return (client)
		? DSA_KEY_ALGORITHM | RSA_KEY_ALGORITHM
		: DSA_KEY_ALGORITHM;
	} else if (keyAlgorithm.equals("NULL")) {
	    return 0;
	} else {
	    throw new AssertionError(
		"Unrecognized key algorithm: " + keyAlgorithm);
	}
    }

    /**
     * Returns true if the algorithm is one of the permitted algorithms,
     * otherwise false.
     */
    static boolean permittedKeyAlgorithm(String keyAlgorithm,
					 int permittedKeyAlgorithms)
    {
	if (permittedKeyAlgorithms == ANY_KEY_ALGORITHM) {
	    return true;
	} else if ("DSA".equals(keyAlgorithm)) {
	    return (permittedKeyAlgorithms & DSA_KEY_ALGORITHM) != 0;
	} else if ("RSA".equals(keyAlgorithm)) {
	    return (permittedKeyAlgorithms & RSA_KEY_ALGORITHM) != 0;
	} else {
	    return false;
	}
    }

    /**
     * Returns the cipher algorithm for the specified cipher suite. <p>
     *
     * The cipher algorithm is found following the first occurrence of "_WITH_"
     * and up to the last underscore.
     */
    static String getCipherAlgorithm(String cipherSuite) {
	int start = cipherSuite.indexOf("_WITH_") + 6;
	if (start >= 6) {
	    int end = cipherSuite.lastIndexOf('_');
	    if (end >= start) {
		return cipherSuite.substring(start, end);
	    }
	}
	return "NULL";
    }

    /**
     * Returns true if the cipher algorithm for the specified cipher suite is
     * considered a strong cipher, otherwise false.
     */
    static boolean hasStrongCipherAlgorithm(String cipherSuite) {
	String cipher = getCipherAlgorithm(cipherSuite);
	return position(cipher, STRONG_ENCRYPTION_CIPHERS) != -1;
    }

    /**
     * Returns the message integrity code algorithm for the specified cipher
     * suite. <p>
     *
     * The message integrity algorithm is found after the last underscore.
     */
    private static String getMICAlgorithm(String cipherSuite) {
	return cipherSuite.substring(cipherSuite.lastIndexOf('_'));
    }

    /**
     * Checks if the suite is supported by this provider.  The suite can only
     * be supported if its security characteristics can be determined, meaning
     * its key exchange and cipher algorithms must be known.
     */
    private static boolean supportedCipherSuite(String cipherSuite) {
	return position(getKeyExchangeAlgorithm(cipherSuite),
			SUPPORTED_KEY_EXCHANGE_ALGORITHMS) != -1 &&
	    position(getCipherAlgorithm(cipherSuite),
		     SUPPORTED_ENCRYPTION_CIPHERS) != -1;
    }

    /* -- subjectString -- */

    /** Returns a String that includes relevant information about a Subject */
    static String subjectString(Subject subject) {
	if (subject == null) {
	    return "null subject";
	} else {
	    return "Subject@" +
		Integer.toHexString(System.identityHashCode(subject)) +
		"{\n" + SubjectCredentials.credentialsString(subject) + "}";
	}
    }

    /* -- safeEquals -- */

    /** Same as equals(), but allows either argument to be null */
    static boolean safeEquals(Object x, Object y) {
	return (x == null) ? y == null : x.equals(y);
    }

    /* -- contains -- */

    /**
     * Returns true if the array contains an equal element, which may be null.
     */
    static boolean contains(Object[] array, Object element) {
	for (int i = array.length; --i >= 0; ) {
	    if (safeEquals(array[i], element)) {
		return true;
	    }
	}
	return false;
    }

    /* -- toString -- */

    /** Converts the contents of an Object array to a String. */
    static String toString(Object[] array) {
	if (array == null) {
	    return "null";
	}
	StringBuffer buf = new StringBuffer("[");
	for (int i = 0; i < array.length; i++) {
	    if (i != 0) {
		buf.append(", ");
	    }
	    buf.append(array[i]);
	}
	buf.append("]");
	return buf.toString();
    }

    /* -- equals -- */

    /**
     * Checks if the elements of two arrays are equal.
     *
     * @param x the first array
     * @param y the second array
     * @return true if both arguments are null or both are non-null, are the
     *	       same length, and, for each array index, the elements of each
     *	       array either both null or both non-null and equal
     */
    static boolean equals(Object[] x, Object[] y) {
	if (x == null) {
	    return y == null;
	} else if (y == null) {
	    return false;
	} else if (x.length != y.length) {
	    return false;
	} else {
	    for (int i = x.length; --i >= 0; ) {
		if (!safeEquals(x[i], y[i])) {
		    return false;
		}
	    }
	    return true;
	}
    }

    /* -- getClassName -- */

    /**
     * Returns the class name of an object, without the package or enclosing
     * class prefix.
     */
    static String getClassName(Object object) {
	String className = object.getClass().getName();
	className = className.substring(className.lastIndexOf('.') + 1);
	className = className.substring(className.lastIndexOf('$') + 1);
	return className;
    }

    /* -- position -- */

    /**
     * Returns the offset of a string in an array of strings.  Returns -1 if
     * the string is not found.
     */
    static int position(String string, String[] list) {
	for (int i = list.length; --i >= 0; ) {
	    if (string.equals(list[i])) {
		return i;
	    }
	}
	return -1;
    }

    /* -- Logging -- */

    /**
     * Logs a throw. Use this method to log a throw when the log message needs
     * parameters.
     *
     * @param logger logger to log to
     * @param level the log level
     * @param sourceClass class where throw occurred
     * @param sourceMethod name of the method where throw occurred
     * @param msg log message
     * @param params log message parameters
     * @param e exception thrown
     */
    static void logThrow(Logger logger,
			 Level level,
			 Class sourceClass,
			 String sourceMethod,
			 String msg,
			 Object[] params,
			 Throwable e)
    {
	LogRecord r = new LogRecord(level, msg);
	r.setLoggerName(logger.getName());
	r.setSourceClassName(sourceClass.getName());
	r.setSourceMethodName(sourceMethod);
	r.setParameters(params);
	r.setThrown(e);
	logger.log(r);
    }
}
