/*
 * 
 * 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 java.math.BigInteger;
import java.security.Key;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.PrivilegedAction;
import java.security.PublicKey;
import java.security.cert.CertPath;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.x500.X500Principal;
import javax.security.auth.x500.X500PrivateCredential;
import net.jini.security.Security;

/**
 * Provides methods for retrieving credentials from a Subject.
 *
 * @author Sun Microsystems, Inc.
 */
class SubjectCredentials extends Utilities {

    /** This class shouldn't be instantiated */
    private SubjectCredentials() { }

    /**
     * Retrieves the X.509 CertPath for a credential name.  Returns null if the
     * chain associated with the credential name is not found.  Does not check
     * if either principal or private key associated with the chain are
     * present.
     *
     * @param subject the Subject containing the credentials or null
     * @param name the name of the credentials
     * @return the certificate chain or null
     */
    static CertPath getCertificateChain(Subject subject, String name) {
	if (subject == null) {
	    return null;
	}
	CertificateMatcher matcher = CertificateMatcher.create(name);
	if (matcher != null) {
	    Set publicCreds = subject.getPublicCredentials();
	    synchronized (publicCreds) {
		for (Iterator it = publicCreds.iterator(); it.hasNext(); ) {
		    Object cred = it.next();
		    if (isX509CertificateChain(cred)) {
			CertPath chain = (CertPath) cred;
			if (matcher.matches(firstX509Cert(chain))) {
			    return chain;
			}
		    }
		}
	    }
	}
	return null;
    }

    /**
     * Returns the credential name for an X.509 certificate.
     *
     * @param cert the certificate
     * @return the credential name
     */
    static String getCertificateName(X509Certificate cert) {
	return CertificateMatcher.getName(cert);
    }

    /**
     * Provides utilities for converting between X.509 certificates and unique
     * certificate names.
     */
    private static class CertificateMatcher {
	private final BigInteger serialNumber;
	private final String issuerName;

	/**
	 * Creates an object that can be compared with an X.509 certificate.
	 * Returns null if the argument is not a valid certificate name.
	 */
	static CertificateMatcher create(String certificateName) {
	    if (certificateName == null) {
		return null;
	    }
	    int atSignPosition = certificateName.indexOf('@');
	    if (atSignPosition < 0) {
		return null;
	    }
	    BigInteger serialNumber;
	    try {
		serialNumber = new BigInteger(
		    certificateName.substring(0, atSignPosition),
		    16);
	    } catch (NumberFormatException e) {
		return null;
	    }
	    String issuerName = certificateName.substring(atSignPosition + 1);
	    return new CertificateMatcher(serialNumber, issuerName);
	}

	private CertificateMatcher(BigInteger serialNumber, String issuerName)
	{
	    this.serialNumber = serialNumber;
	    this.issuerName = issuerName;
	}

	/** Returns the unique certificate name for an X.509 certificate */
	static String getName(X509Certificate certificate) {
	    /*
	     * Use the certificate serial number, which is unique for all
	     * certificates from a given issuer, plus the issuer name, to get a
	     * unique name for the certificate.
	     */
	    return certificate.getSerialNumber().toString(16) + "@" +
		getIssuerName(certificate);
	}

	/**
	 * Returns true if an X.509 certificate matches the certificate name
	 * specified in the constructor.
	 */
	boolean matches(X509Certificate certificate) {
	    return certificate.getSerialNumber().equals(serialNumber)
		&& getIssuerName(certificate).equals(issuerName);
	}

	/** Returns the canonical issuer name for an X.509 certificate. */
	private static String getIssuerName(X509Certificate certificate) {
	    return certificate.getIssuerX500Principal().getName(
		X500Principal.CANONICAL);
	}
    }

    /**
     * Returns the X.509 CertPaths stored in the public credentials of the
     * subject.  Does not check if the associated principals or private keys
     * are present.  Returns null if none are found.
     *
     * @param subject the subject containing the X.509 CertPaths or null
     * @return List of the X.509 CertPaths in the subject
     */
    static List getCertificateChains(Subject subject) {
	List result = null;
	if (subject != null) {
	    Set publicCreds = subject.getPublicCredentials();
	    synchronized (publicCreds) {
		for (Iterator it = publicCreds.iterator(); it.hasNext(); ) {
		    Object cred = it.next();
		    if (isX509CertificateChain(cred)) {
			if (result == null) {
			    result = new ArrayList(publicCreds.size());
			}
			result.add(cred);
		    }
		}
	    }
	}
	return result;
    }	    

    /**
     * Checks if the subject's public credentials contain a certificate chain
     * that starts with a certificate with the same subject and public key, and
     * returns the certificate chain if it does.  Does not check the validity
     * of the certificate chain, or for associated private credentials or
     * principal.
     *
     * @param cert the certificate
     * @return the certificate chain starting with an equivalent certificate,
     *	       if present, otherwise null
     */
    static CertPath getCertificateChain(Subject subject, X509Certificate cert)
    {
	if (subject != null) {
	    Principal subjectDN = null;
	    PublicKey key = null;
	    Set publicCreds = subject.getPublicCredentials();
	    synchronized (publicCreds) {
		for (Iterator it = publicCreds.iterator(); it.hasNext(); ) {
		    Object cred = it.next();
		    if (!isX509CertificateChain(cred)) {
			continue;
		    }
		    CertPath chain = (CertPath) cred;
		    X509Certificate start = firstX509Cert(chain);
		    if (cert.equals(start)) {
			return chain;
		    }
		    if (subjectDN == null) {
			subjectDN = cert.getSubjectDN();
			key = cert.getPublicKey();
		    }
		    if (subjectDN.equals(start.getSubjectDN())
			&& key.equals(start.getPublicKey()))
		    {
			return chain;
		    }
		}
	    }
	}
	return null;
    }

    /**
     * Retrieves the principals in the subject with X.509 CertPaths which use
     * the specified key algorithm and, optionally, have associated private
     * credentials.  Uses the specified private credentials rather than getting
     * them from the Subject to permit callers to cache them and avoid the cost
     * of repeated permission checks to access them.
     *
     * @param subject the Subject containing the principals
     * @param keyAlgorithms the permitted key algorithms, an OR of any of
     *	      DSA_KEY_ALGORITHM and RSA_KEY_ALGORITHM
     * @param privateCredentials the available private credentials, or null if
     *	      private credentials are not required
     * @return set of matching principals
     */
    static Set getPrincipals(Subject subject,
			     int keyAlgorithms,
			     X500PrivateCredential[] privateCredentials)
    {
	Set result = new HashSet(subject.getPrincipals().size());
	List certPaths = getCertificateChains(subject);
	if (certPaths != null) {
	    for (int i = certPaths.size(); --i >= 0; ) {
		CertPath chain = (CertPath) certPaths.get(i);
		X509Certificate cert = firstX509Cert(chain);
		String alg = cert.getPublicKey().getAlgorithm();
		if (!permittedKeyAlgorithm(alg, keyAlgorithms)) {
		    continue;
		}
		X500Principal principal = getPrincipal(subject, cert);
		if (principal != null) {
		    boolean pcOK = privateCredentials == null;
		    if (!pcOK) {
			for (int j = privateCredentials.length; --j >= 0; ) {
			    X500PrivateCredential xpc = privateCredentials[j];
			    if (cert.equals(xpc.getCertificate())) {
				pcOK = true;
				break;
			    }
			}
		    }
		    if (pcOK) {
			result.add(principal);
		    }
		}
	    }
	}
	return result;
    }

    /**
     * A privileged action that gets the private credentials for an X.509
     * certificate.
     */
    static class GetPrivateCredentialAction implements PrivilegedAction {
	private final Subject subject;
	private final X509Certificate cert;

	GetPrivateCredentialAction(Subject subject, X509Certificate cert) {
	    this.subject = subject;
	    this.cert = cert;
	}

	public Object run() {
	    return SubjectCredentials.getPrivateCredential(subject, cert);
	}
    }

    /**
     * A privileged action that returns all the X.500 private credentials for a
     * subject as an X500PrivateCredential array.  Assumes that the subject is
     * non-null.
     */
    static class GetAllPrivateCredentialsAction implements PrivilegedAction {
	private final Subject subject;

	GetAllPrivateCredentialsAction(Subject subject) {
	    this.subject = subject;
	}

	public Object run() {
	    Set pcs = subject.getPrivateCredentials();
	    List xpcs = new ArrayList(pcs.size());
	    synchronized (pcs) {
		/*
		 * XXX: Include this synchronization to work around BugID
		 * 4892913, Subject.getPrivateCredentials not thread-safe
		 * against changes to principals.  -tjb[22.Jul.2003]
		 *
		 * synchronized (subject.getPrincipals()) {
		 */
		for (Iterator iter = pcs.iterator(); iter.hasNext(); ) {
		    Object pc = iter.next();
		    if (pc instanceof X500PrivateCredential) {
			xpcs.add(pc);
		    }
		}
	    }
	    return xpcs.toArray(new X500PrivateCredential[xpcs.size()]);
	}
    }

    /**
     * Returns the X500PrivateCredential for an X.509 certificate.  Returns
     * null if the associated private credential is missing from the subject.
     * Does not check if the public credential or principal are present.
     * Assumes that the subject is non-null.  The caller should check for
     * AuthenticationPermission and then call this method from within
     * AccessController.doPrivileged to give it private credential permissions.
     *
     * @param subject the Subject containing the credentials
     * @param cert the X.509 certificate
     * @return the X500PrivateCredential or null
     */
    static X500PrivateCredential getPrivateCredential(Subject subject,
						      X509Certificate cert)
    {
	X500PrivateCredential result = null;
	Set privateCreds = subject.getPrivateCredentials();
	synchronized (privateCreds) {
	    /*
	     * XXX: Include this synchronization to work around BugID 4892913,
	     * Subject.getPrivateCredentials not thread-safe against changes to
	     * principals.  -tjb[18.Jul.2003]
	     *
	     * synchronized (subject.getPrincipals()) {
	     */
	    for (Iterator it = privateCreds.iterator(); it.hasNext(); ) {
		Object cred = it.next();
		if (cred instanceof X500PrivateCredential) {
		    X500PrivateCredential xpc =
			(X500PrivateCredential) cred;
		    if (cert.equals(xpc.getCertificate())) {
			result = xpc;
			break;
		    }
		}
	    }
	}
	return result;
    }

    /**
     * Returns the subject principal matching the X.509 certificate.  Returns
     * null if the principal is not found.  Does not check if the associated
     * private key is present.  Assumes that the subject is non-null.
     *
     * @param subject the Subject containing the credentials 
     * @param cert the X.509 certificate
     * @return the X.500 principal or null
     */
    static X500Principal getPrincipal(Subject subject, X509Certificate cert) {
	X500Principal x500 = cert.getSubjectX500Principal();
	String name = x500.getName(X500Principal.CANONICAL);
	Set principals = subject.getPrincipals();
	synchronized (principals) {
	    for (Iterator i = principals.iterator(); i.hasNext(); ) {
		Object next = i.next();
		if (!(next instanceof X500Principal)) {
		    continue;
		}
		X500Principal principal = (X500Principal) next;
		if (principal.getName(X500Principal.CANONICAL).equals(name)) {
		    return principal;
		}
	    }
	}
	return null;
    }

    /**
     * Returns a String that describes the credentials in the subject.
     *
     * @param subject the Subject containing the credentials
     * @return a String describing the credentials
     * @throws NullPointerException if the subject is null
     */
    static String credentialsString(Subject subject) {
	List certPaths = getCertificateChains(subject);
	if (certPaths == null) {
	    return "";
	}
	StringBuffer buf = new StringBuffer();
	for (int i = certPaths.size(); --i >= 0; ) {
	    CertPath chain = (CertPath) certPaths.get(i);
	    X509Certificate cert = firstX509Cert(chain);
	    X500Principal principal = getPrincipal(subject, cert);
	    if (principal != null) {
		buf.append("  Principal: ").append(principal).append('\n');
		buf.append("    Public key: ");
		appendKeyString(cert.getPublicKey(), buf);
		buf.append('\n');
		buf.append("    Private key: ");
		try {
		    X500PrivateCredential cred =
			(X500PrivateCredential) Security.doPrivileged(
			    new GetPrivateCredentialAction(subject, cert));
		    PrivateKey privateKey =
			cred != null ? cred.getPrivateKey() : null;
		    if (privateKey == null) {
			buf.append("Not found");
		    } else {
			appendKeyString(privateKey, buf);
		    }
		} catch (SecurityException e) {
		    buf.append("No permission");
		}
	    }
	}
	return buf.toString();
    }

    /** Appends information about a key to a StringBuffer. */
    private static void appendKeyString(Key key, StringBuffer buf) {
	String className = key.getClass().getName();
	buf.append(className.substring(className.lastIndexOf('.') + 1));
	buf.append('@');
	buf.append(Integer.toHexString(System.identityHashCode(key)));
    }

    /**
     * Determines if the argument is an X.509 certificate CertPath.  Returns
     * true if the argument is a non-null CertPath, has at least one
     * certificate, and has type X.509.
     */
    private static boolean isX509CertificateChain(Object credential) {
	if (!(credential instanceof CertPath)) {
	    return false;
	}
	CertPath certPath = (CertPath) credential;
	if (certPath.getCertificates().isEmpty()) {
	    return false;
	} else if (!certPath.getType().equals("X.509")) {
	    return false;
	}
	return true;
    }
}
