/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (the License).  You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the license at 
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * Header Notice in each file and include the License file 
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.  
 * If applicable, add the following below the CDDL Header, 
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information: 
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 */

/*
 * WSSCallbackHandler.java
 *
 * Created on April 21, 2004, 11:56 AM
 */

package com.sun.enterprise.webservice;

import java.io.IOException;
import java.math.BigInteger;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.KeyStoreException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertStore;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.crypto.SecretKey;
import javax.security.auth.x500.X500Principal;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;

import com.sun.enterprise.security.jauth.callback.CertStoreCallback;
import com.sun.enterprise.security.jauth.callback.PasswordValidationCallback;
import com.sun.enterprise.security.jauth.callback.PrivateKeyCallback;
import com.sun.enterprise.security.jauth.callback.SecretKeyCallback;
import com.sun.enterprise.security.jauth.callback.TrustStoreCallback;

import com.sun.enterprise.Switch;
import com.sun.enterprise.security.SecurityUtil;
import com.sun.enterprise.security.SSLUtils;
import com.sun.enterprise.security.auth.LoginContextDriver;
import com.sun.enterprise.security.LoginException;
import com.sun.enterprise.security.auth.realm.Realm;
import com.sun.enterprise.webservice.AppclientWSSCallbackHandler;
import com.sun.enterprise.webservice.EjbServletWSSCallbackHandler;
import com.sun.logging.LogDomains;

import sun.security.util.DerValue;

/**
 * Callback Handler for WebServices Security
 * @author  Harpreet Singh
 * @author  Shing Wai Chan
 */
public abstract class WSSCallbackHandler implements CallbackHandler {
    private static String SUBJECT_KEY_IDENTIFIER_OID = "2.5.29.14";

    protected static Logger _logger = LogDomains.getLogger(LogDomains.SECURITY_LOGGER);

    /**
     * temporary make this method public. refer RealmAdapter.java.
     */
    static public CallbackHandler getInstance() {
        if (Switch.getSwitch().getContainerType() == Switch.APPCLIENT_CONTAINER) {
            return AppclientWSSCallbackHandler.newInstance();
        } else {
            return EjbServletWSSCallbackHandler.newInstance();
        }
    }

    /*
     * To be implemented by a sub-class. The sub class decides 
     * which callbacks it supports.
     * <i>EjbServletWSSCallbackHandler</i> supports:
     * <li>SecretKeyCallback</li>
     * <li>TrustStoreCallback</li>
     * <li>PasswordValidationCallback</li>
     * <li>CertStoreCallback</li>
     * <li>PrivateKeyCallback</li>
     * <i> AppclientWSSCallbackHandler</i> supports:
     * <li>NameCallback</li>
     * <li>PasswordCallback</li>
     * <li>ChoiceCallback</li>
     */
    abstract boolean isSupportedCallback(Callback callback);
    
    public void handle(Callback[] callbacks) 
        throws IOException, UnsupportedCallbackException{};
    
    /**
     * gets the appropriate callback processor and hands the callback to 
     * processor to process the callback.
     */
    protected void processCallback (Callback callback) 
            throws UnsupportedCallbackException {
    	if (callback instanceof PasswordValidationCallback) {
            processPasswordValidation((PasswordValidationCallback)callback);
        } else if (callback instanceof PrivateKeyCallback) {
            processPrivateKey((PrivateKeyCallback)callback);
        } else if (callback instanceof TrustStoreCallback) {
            TrustStoreCallback tstoreCallback = (TrustStoreCallback)callback;
            if (_logger.isLoggable(Level.FINE)) {
                _logger.log(Level.FINE, 
                "container-auth: wss : In TrustStoreCallback Processor");
            }
            tstoreCallback.setStore (SSLUtils.getMergedTrustStore());

        } else if (callback instanceof CertStoreCallback) {
            processCertStore((CertStoreCallback)callback);
        } else if (callback instanceof SecretKeyCallback) {
            processSecretKey((SecretKeyCallback)callback);
        } else {
            // sanity check =- should never come here.
            // the isSupportedCallback method already takes care of this case
            _logger.log(Level.FINE,"wss-container-auth: UnsupportedCallback : "+
                    callback.getClass().getName());
            throw new UnsupportedCallbackException(callback); 
        }
    }

    private void processPasswordValidation(
            PasswordValidationCallback pwdCallback) {

        if (Switch.getSwitch().getContainerType() == Switch.APPCLIENT_CONTAINER) {
            if (_logger.isLoggable(Level.FINE)){
                _logger.log(Level.FINE, "container-auth: wss : In PasswordValidationCallback Processor for appclient - will do nothing");
            }
            pwdCallback.setResult(true);
            return;
        }
        String username = pwdCallback.getUsername();
        String password = new String(pwdCallback.getPassword());
        if (_logger.isLoggable(Level.FINE)) {
            _logger.log(Level.FINE, "container-auth: wss : In PasswordValidationCallback Processor");
        }
        try {
            String defaultRealm = Realm.getDefaultRealm();
            LoginContextDriver.wssLoginUsernamePassword(username, password, 
                defaultRealm);
            if(_logger.isLoggable(Level.FINE)){
                _logger.log(Level.FINE, 
                    "container-auth wss: authentication succeeded for user = ", 
                    username);
            }
            // explicitly ditch the password
            password = null;
            pwdCallback.setResult(true);
        } catch(LoginException le) {
            // login failed
            _logger.log(Level.INFO, 
                "container-auth: wss : Login failed for user :",
                username);
            pwdCallback.setResult(false);
        }
    }

    private void processPrivateKey(PrivateKeyCallback privKeyCallback) {
        KeyStore[] kstores = SecurityUtil.getSecuritySupport().getKeyStores();
        if (_logger.isLoggable(Level.FINE)) {
            _logger.log(Level.FINE, 
                "container-auth: wss : In PrivateKeyCallback Processor");
        }
    	
        // make sure we have a keystore
        if (kstores == null || kstores.length == 0) {
            // cannot get any information
            privKeyCallback.setKey(null, null);
            return;
        }

        String[] passwords =
            SecurityUtil.getSecuritySupport().getKeyStorePasswords();

        // get the request type
        PrivateKeyCallback.Request req = privKeyCallback.getRequest();
        if (req == null) {
            // no request type - set default key
            setDefaultKey(privKeyCallback, kstores, passwords);
            passwords = null;
            return;
        }

        // find key based on request type
        PrivateKey privKey = null;
        Certificate[] certs = null;
        try {
            if (req instanceof PrivateKeyCallback.AliasRequest) {
                PrivateKeyCallback.AliasRequest aReq =
                        (PrivateKeyCallback.AliasRequest)req;

                String alias = aReq.getAlias();
                PrivateKeyEntry privKeyEntry = null;
                if (alias == null) {
                    // set default key
                    setDefaultKey(privKeyCallback, kstores, passwords);
                    passwords = null;
                    return;
                } else if ((privKeyEntry = SSLUtils.getPrivateKeyEntryFromTokenAlias(alias)) != null) {
                    privKey = privKeyEntry.getPrivateKey();
                    certs = privKeyEntry.getCertificateChain();
                }
            } else if (req instanceof PrivateKeyCallback.IssuerSerialNumRequest) {
                PrivateKeyCallback.IssuerSerialNumRequest isReq =
                        (PrivateKeyCallback.IssuerSerialNumRequest)req;
                X500Principal issuer = isReq.getIssuer();
                BigInteger serialNum = isReq.getSerialNum();
                if (issuer != null && serialNum != null) {
                    boolean found = false;
                    for (int i = 0; i < kstores.length && !found; i++) {
                        Enumeration aliases = kstores[i].aliases();
                        while (aliases.hasMoreElements() && !found) {
                            String nextAlias = (String)aliases.nextElement();
                            Key key = kstores[i].getKey(nextAlias, passwords[i].toCharArray());
                            if (key != null && (key instanceof PrivateKey)) {
                                Certificate[] certificates =
                                        kstores[i].getCertificateChain(nextAlias);
                                // check issuer/serial
                                X509Certificate eeCert = (X509Certificate)certificates[0];
                                if (eeCert.getIssuerX500Principal().equals(issuer) &&
                                        eeCert.getSerialNumber().equals(serialNum)) {
                                    privKey = (PrivateKey)key;
                                    certs = certificates;
                                    found = true;
                                }
                            }
                        }
                    }
                }
            } else if (req instanceof PrivateKeyCallback.SubjectKeyIDRequest) {
                PrivateKeyCallback.SubjectKeyIDRequest skReq = 
                        (PrivateKeyCallback.SubjectKeyIDRequest)req;
                byte[] subjectKeyID = skReq.getSubjectKeyID();
                if (subjectKeyID != null) {
                    boolean found = false;
                    // In DER, subjectKeyID will be an OCTET STRING of OCTET STRING
                    DerValue derValue1 = new DerValue(
                        DerValue.tag_OctetString, subjectKeyID);
                    DerValue derValue2 = new DerValue(
                        DerValue.tag_OctetString, derValue1.toByteArray());
                    byte[] derSubjectKeyID = derValue2.toByteArray();

                    for (int i = 0; i < kstores.length && !found; i++) {
                        Enumeration aliases = kstores[i].aliases();
                        while (aliases.hasMoreElements() && !found) {
                            String nextAlias = (String)aliases.nextElement();
                            Key key = kstores[i].getKey(nextAlias, passwords[i].toCharArray());
                            if (key != null && (key instanceof PrivateKey)) {
                                Certificate[] certificates =
                                        kstores[i].getCertificateChain(nextAlias);
                                X509Certificate eeCert = (X509Certificate)certificates[0];
                                // Extension: SubjectKeyIdentifier
                                byte[] derSubKeyID = eeCert.getExtensionValue(SUBJECT_KEY_IDENTIFIER_OID);
                                if (derSubKeyID != null &&
                                        Arrays.equals(derSubKeyID, derSubjectKeyID)) {
                                    privKey = (PrivateKey)key;
                                    certs = certificates;
                                    found = true;
                                }
                            }
                        }
                    }
                }
            } else {
                if (_logger.isLoggable(Level.FINE)) {
                    _logger.log(Level.FINE,
                         "invalid request type: " + req.getClass().getName());
                }
            }
         } catch (Exception e) {
             // UnrecoverableKeyException
             // NoSuchAlgorithmException
             // KeyStoreException
             if (_logger.isLoggable(Level.FINE)) {
                 _logger.log(Level.FINE,
                 "container-auth: wss : In PrivateKeyCallback Processor: " +
                 " Error reading key !", e);
             }
         } finally {
             privKeyCallback.setKey(privKey, certs);
             passwords = null;
         }
    }
    
    /**
     * Return the first key/chain that we can successfully
     * get out of the keystore
     */
    private void setDefaultKey(PrivateKeyCallback privKeyCallback,
            KeyStore[] kstores, String[] passwords) {
        PrivateKey privKey = null;
        Certificate[] certs = null;
        try {
            for (int i = 0; i < kstores.length && privKey == null; i++) {
                Enumeration aliases = kstores[i].aliases();
                // loop thru aliases and try to get the key/chain
                while (aliases.hasMoreElements() && privKey == null) {
                    String nextAlias = (String)aliases.nextElement();
                    privKey = null;
                    certs = null;
                    Key key = kstores[i].getKey(nextAlias, passwords[i].toCharArray());
                    if (key != null && (key instanceof PrivateKey)) {
                        privKey = (PrivateKey)key;
                        certs = kstores[i].getCertificateChain(nextAlias);
                    }
                }
            }
        } catch (Exception e) {
            // UnrecoverableKeyException
            // NoSuchAlgorithmException
            // KeyStoreException
        }
    	
        privKeyCallback.setKey(privKey, certs);
    }

    private void processCertStore(CertStoreCallback certStoreCallback) {
        if (_logger.isLoggable(Level.FINE)) {
            _logger.log(Level.FINE, 
                "container-auth: wss : In CertStoreCallback Processor");
        }

        KeyStore certStore = SSLUtils.getMergedTrustStore();
        if (certStore == null) {// should never happen
            certStoreCallback.setStore((CertStore)null);
        }
        List list = new ArrayList();
        CollectionCertStoreParameters ccsp = null;
        try{
            Enumeration enu = certStore.aliases();
            while (enu.hasMoreElements()) {
                String alias = (String)enu.nextElement();
                if(certStore.isCertificateEntry(alias)){
                    try{
                        Certificate cert = certStore.getCertificate(alias);
                        list.add(cert);
                    }catch(KeyStoreException kse){
                        // ignore and move to next
                        _logger.log(Level.FINE, "container-auth : wss: Cannot retrieve" +
                        "certificate for alias "+alias);
                    }
                }
            }
            ccsp = new CollectionCertStoreParameters(list);
            CertStore certstore = CertStore.getInstance("Collection", ccsp);
            certStoreCallback.setStore(certstore);
        } catch(KeyStoreException kse){
            _logger.log(Level.FINE, 
                "container-auth: wss :  Cannot determine truststore aliases", kse);        
        } catch(InvalidAlgorithmParameterException iape){
            _logger.log(Level.FINE, 
                "container-auth: wss :  Cannot instantiate CertStore", iape);        
        } catch(NoSuchAlgorithmException nsape){
            _logger.log(Level.FINE, 
                "container-auth: wss :  Cannot instantiate CertStore", nsape);        
        }
    }

    private void processSecretKey(SecretKeyCallback secretKeyCallback) {
        if (_logger.isLoggable(Level.FINE)) {
            _logger.log(Level.FINE, 
                "container-auth: wss : In SecretKeyCallback Processor");
        }

        KeyStore secretStore = SecurityUtil.getSecuritySupport().getKeyStores()[0];
        if (secretStore == null) {
            // cannot get any information
            secretKeyCallback.setKey(null);
        }
        String alias = ((SecretKeyCallback.AliasRequest)secretKeyCallback.getRequest()).getAlias();
        if (alias != null) {
           // XXX modify SecuritySupport to get the password for the keystore
           String pass = SSLUtils.getKeyStorePass();
           try {
                Key key = secretStore.getKey(alias, pass.toCharArray());
                if (key instanceof SecretKey) {
                    secretKeyCallback.setKey((SecretKey)key);
                } else {
                    secretKeyCallback.setKey(null);
                }
            } catch(Exception e) {
                if (_logger.isLoggable(Level.FINE)) {
                    _logger.log(Level.FINE, 
                    "container-auth: wss : In SecretKeyCallback Processor: "+
                    " Error reading key ! for alias "+alias, e);
                }
                secretKeyCallback.setKey(null);
            } finally {
                pass = null;
            }
        } else {
            // Dont bother about checking for principal
            // we dont support that feature - typically 
            // used in an environment with kerberos
            //            Principal p = secretKeyCallback.getPrincipal();
            secretKeyCallback.setKey(null);
            if (_logger.isLoggable(Level.WARNING)) {
                _logger.log(Level.WARNING, 
                    "container-auth: wss : No support to read Principals in SecretKeyCallback");
            }
        }
    }
}
