/*
 *   
 *
 * Copyright  1990-2006 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation. 
 * 
 * This program 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 GNU
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt). 
 * 
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA 
 * 
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions. 
 */

package com.sun.satsa.pki;

import com.sun.midp.io.j2me.storage.File;
import com.sun.midp.io.j2me.storage.RandomAccessStream;
import com.sun.midp.io.j2me.apdu.APDUManager;
import com.sun.midp.i18n.Resource;
import com.sun.midp.i18n.ResourceConstants;
import com.sun.midp.midlet.MIDletStateHandler;
import com.sun.midp.main.Configuration;
import com.sun.midp.configurator.Constants;
import com.sun.midp.security.ImplicitlyTrustedClass;
import com.sun.midp.security.SecurityToken;
import com.sun.satsa.security.SecurityInitializer;
import com.sun.satsa.util.*;

import javax.microedition.io.Connector;
import javax.microedition.pki.UserCredentialManager;
import javax.microedition.pki.UserCredentialManagerException;
import javax.microedition.securityservice.CMSMessageSignatureServiceException;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Vector;
import com.sun.midp.security.Permissions;

/**
 * This class provides implementation of methods defined by
 * javax.microedition.pki.UserCredentialManager and
 * javax.microedition.securityservice.CMSMessageSignatureService
 * classes.
 */
public class PKIManager {

    /** Signature operation identifier. */
    public static final int AUTHENTICATE_DATA      = 0;
    /** Signature operation identifier. */
    public static final int AUTHENTICATE_STRING    = 1;
    /** Signature operation identifier. */
    public static final int SIGN_STRING            = 2;

    /**
     * Inner class to request security token from SecurityInitializer.
     * SecurityInitializer should be able to check this inner class name.
     */
    static private class SecurityTrusted
        implements ImplicitlyTrustedClass {};

    /** This class has a different security domain than the MIDlet suite */
    private static SecurityToken classSecurityToken =
        SecurityInitializer.requestToken(new SecurityTrusted());

    /**
     * Storage name for identifiers of public keys for which
     * certificates are expected. */
    private static final String CSR_ID_FILE =  "_csr.id";

    /**
     * Contains identifiers of public keys for which certificates are
     * expected if this information is not stored in persistent storage.
     */
    private static Vector tmpCSRList;

    /**
     * Creates a DER encoded PKCS#10 certificate enrollment request.
     * @param nameInfo the distinguished name to be included in the
     * PKCS#10 certificate signing request.
     * @param algorithm the Object Identifier (OID) for the public key
     * algorithm to use.
     * @param keyLen the key length.
     * @param keyUsage the functionality for which the key is marked
     * inside the security element.
     * @param securityElementID identifies the security element on which
     * the key resides.
     * @param securityElementPrompt guides a user to insert the correct
     * security element, if a suitable security element is removable and
     * not detected.
     * @param forceKeyGen if set to true a new key MUST be generated.
     * @return DER encoded PKCS#10 certificate enrollment request
     * @throws UserCredentialManagerException if an error occurs while
     * generating the certificate request
     * @throws CMSMessageSignatureServiceException if an error occurs
     * while signing the certificate request
     * @throws SecurityException if a PIN is blocked due to an excessive
     * number of incorrect PIN entries
     */
    public static synchronized byte[] generateCSR(String nameInfo,
            String algorithm, int keyLen, int keyUsage,
            String securityElementID, String securityElementPrompt,
            boolean forceKeyGen) throws UserCredentialManagerException,
                                    CMSMessageSignatureServiceException {

        if (nameInfo != null && nameInfo.trim().length() == 0) {
            throw new IllegalArgumentException("Invalid name");
        }

        try {
            Utils.StringToOID(algorithm);
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException("Invalid algorithm");
        }

        if (keyLen <= 0  || keyLen > 20480) {
            throw new IllegalArgumentException("Invalid key length");
        }

        if (keyUsage != UserCredentialManager.KEY_USAGE_AUTHENTICATION &&
            keyUsage != UserCredentialManager.KEY_USAGE_NON_REPUDIATION) {
                throw new IllegalArgumentException("Invalid key usage");
        }

        if (! algorithm.equals(UserCredentialManager.ALGORITHM_RSA)) {
            throw new UserCredentialManagerException(
                    UserCredentialManagerException.SE_NO_KEYS);
        }

        int slotCount = APDUManager.getSlotCount();

        while (true) {

            for (int i = 0; i < slotCount; i++) {

                WIMApplication w = WIMApplication.getInstance(
                        classSecurityToken, i, securityElementID, false);
                if (w == null) {
                    continue;
                }
                try {
                    Vector CSRs = loadCSRList();
                    byte[] CSR = w.generateCSR(nameInfo, keyLen,
                                           keyUsage, forceKeyGen, CSRs);
                    storeCSRList(CSRs);
                    return CSR;
                } finally {
                    w.done();
                }
            }

            // WIM application is not found

            if (securityElementPrompt != null) {
                try {
                    if (MessageDialog.showMessage(classSecurityToken,
                        Resource.getString(ResourceConstants
					   .JSR177_WIM_NOT_FOUND),
                        securityElementPrompt,
                        true) != -1) {
                        continue;
                    }
                } catch (InterruptedException e) {}
            }
            throw new UserCredentialManagerException(
                          UserCredentialManagerException.SE_NOT_FOUND);
        }
    }

    /**
     * Adds a user certificate to a certificate store.
     * @param certDisplayName the user friendly name associated with the
     * certificate.
     * @param pkiPath the DER encoded PKIPath containing user
     * certificate and certificate authority certificates.
     * @param uri a URI that resolves to a X.509v3 certificate.
     * @return true if successful
     * @throws UserCredentialManagerException if an error occurs while
     * adding a user credential
     * @throws SecurityException if a PIN is blocked due to an excessive
     * number of incorrect PIN entries
     */
    public static synchronized boolean addCredential(String certDisplayName,
            byte[] pkiPath, String uri)
            throws UserCredentialManagerException {

        // check parameters

        if (certDisplayName == null ||
            certDisplayName.trim().length() == 0) {
            throw new IllegalArgumentException("Invalid name");
        }
        certDisplayName = certDisplayName.trim();

        TLV t = null;
        String info = null;

        try {
            t = new TLV(pkiPath, 0).child;
            TLV v = t;
            if (v != null) {
                while (v.next != null) {
                    v = v.next;
                }
                info = Certificate.getInfo(v);
            }
        } catch (TLVException e) {} // ignored

        if (info == null) {
            throw new IllegalArgumentException("Invalid pkiPath");
        }

        // ask user

        try {
            if (MessageDialog.showMessage(classSecurityToken,
                Resource.getString(ResourceConstants.AMS_CONFIRMATION),
                Resource.getString(ResourceConstants.JSR177_CERTIFICATE_STORED) +
                "\n\n" + 
                // "Label" +
                Resource.getString(ResourceConstants.JSR177_CERTIFICATE_LABEL) +
                ": " + certDisplayName + "\n\n" +
                info + "\n\n",
                true) == Dialog.CANCELLED) {
                return false;
            }
        } catch (InterruptedException e) {
            return false;
        }

        // save certificates

        Vector CSRs = loadCSRList();

        int slotCount = APDUManager.getSlotCount();

        for (int i = 0; i < slotCount; i++) {

            WIMApplication w = WIMApplication.getInstance(
                    classSecurityToken, i, null, false);

            if (w == null) {
                continue;
            }
            try {
                int result = w.addCredential(certDisplayName, t, CSRs);
                if (result == WIMApplication.SUCCESS) {
                    storeCSRList(CSRs);
                    return true;
                }
                if (result == WIMApplication.CANCEL) {
                    return false;
                }
                if (result == WIMApplication.ERROR) {
                    break;
                }
            } catch (IllegalArgumentException e) {
                throw e;
            } catch (SecurityException e) {
                throw e;
            } finally {
                w.done();
            }
        }
        throw new UserCredentialManagerException(
                UserCredentialManagerException.CREDENTIAL_NOT_SAVED);
    }

    /**
     * Removes a certificate from a certificate store.
     * @param certDisplayName the user friendly name associated with the
     * certificate.
     * @param issuerAndSerialNumber the DER encoded ASN.1 structure that
     * contains the certificate issuer and serial number.
     * @param securityElementID identifies the security element on which
     * the key resides.
     * @param securityElementPrompt guides the user to insert the
     * correct security element if the security element is removable and
     * not detected.
     * @return false if operation cancelled
     * @throws UserCredentialManagerException if
     * an error occurs while removing the credential
     * @throws SecurityException if a PIN is blocked due to an excessive
     * number of incorrect PIN entries
     */
    public static boolean removeCredential(String certDisplayName,
                                        byte[] issuerAndSerialNumber,
                                        String securityElementID,
                                        String securityElementPrompt)
            throws UserCredentialManagerException {

        if (certDisplayName == null ||
            certDisplayName.trim().length() == 0) {
            throw new IllegalArgumentException("Invalid name");
        }
        certDisplayName = certDisplayName.trim();

        TLV isn;
        try {
            isn = new TLV(issuerAndSerialNumber, 0);
            /*
               Compare the name with itself to make sure that it is
               properly formatted
            */
            try {
                RFC2253Name.compare(isn.child, isn.child);
            } catch (IllegalArgumentException iae) {
                throw new IllegalArgumentException(
                          "Invalid issuerAndSerialNumber");
            }
            if (isn.child.next == null || isn.child.next.type != TLV.INTEGER_TYPE) {
                throw new IllegalArgumentException(
                          "Invalid issuerAndSerialNumber");
            }
        } catch (TLVException e) {
            throw new IllegalArgumentException(
                                      "Invalid issuerAndSerialNumber");
        }
        int slotCount = APDUManager.getSlotCount();

        while (true) {

            for (int i = 0; i < slotCount; i++) {

                WIMApplication w = WIMApplication.getInstance(
                       classSecurityToken, i, securityElementID, false);
                if (w == null) {
                    continue;
                }
                try {
                    int result = w.removeCredential(certDisplayName, isn);
                    if (result == WIMApplication.SUCCESS) {
                        return true;
                    }
                    if (result == WIMApplication.CANCEL) {
                        return false;
                    }
                    throw new UserCredentialManagerException(
                                         UserCredentialManagerException.
                                         CREDENTIAL_NOT_FOUND);
                } finally {
                    w.done();
                }
            }

            // WIM application is not found

            if (securityElementPrompt != null) {
                try {
                    if (MessageDialog.showMessage(classSecurityToken,
                            Resource.getString(
                                ResourceConstants.JSR177_WIM_NOT_FOUND),
                            securityElementPrompt,
                            true) != -1) {
                        continue;
                    }
                } catch (InterruptedException e) {}
            }
            throw new UserCredentialManagerException(
                          UserCredentialManagerException.SE_NOT_FOUND);
        }
    }


    /**
     * Generates a signature.
     * @param action type of signature operation.
     * @param data data to be signed or null
     * @param string string to be signed or null
     * @param options signature format options
     * @param caNames an array of Strings that contain the distinguished
     * names of trusted certification authorities.
     * @param securityElementPrompt guides a user to insert the correct
     * security element if the security element is removable and not
     * detected.
     * @return the DER encoded signature, null if the signature
     * generation was cancelled by the user before completion
     * @throws CMSMessageSignatureServiceException if an error occurs
     * during signature generation
     * @throws UserCredentialManagerException if key not found
     * @throws SecurityException if caller does not have permission
     */
    public static byte[] sign(int action, byte[] data, String string,
                               int options, String[] caNames,
                               String securityElementPrompt)
            throws CMSMessageSignatureServiceException,
		   UserCredentialManagerException {

        // Only CMSMessageSignatureService.sign() is 
        // protected by MIDP permissions
        if (action == AUTHENTICATE_DATA) { 
            try {
                MIDletStateHandler.getMidletStateHandler().getMIDletSuite().
                        checkForPermission(Permissions.SIGN_SERVICE, null);
            } catch (InterruptedException ie) {
                throw new SecurityException(
                    "Interrupted while trying to ask the user permission");
            }
        }

        if (action == AUTHENTICATE_DATA ?
            (data == null || data.length == 0) :
            (string == null || string.length() == 0)) {
            // IMPL_NOTE: specification ?
            throw new IllegalArgumentException("Invalid data");
        }

        /*
            Parse the CA names, toTLV throws IllegalArgumentException
            if necessary.
        */

        TLV[] names = null;
        if (caNames != null && caNames.length != 0) {
            names = new TLV[caNames.length];
            for (int i = 0; i < caNames.length; i++) {
                names[i] = RFC2253Name.toTLV(caNames[i]);
            }
        }

        // ask user confirmation if necessary
        if (action != AUTHENTICATE_DATA) {
            try {
                if (MessageDialog
		    .showMessage(classSecurityToken,
				 Resource
				 .getString(ResourceConstants
					    .JSR177_CONFIRM_SIGNATURE),
				 Resource
				 .getString(ResourceConstants
                     .JSR177_STRING_TO_SIGN) +
				 string, true) != 1) {
                    return null;
                }
            } catch (InterruptedException e) {
                throw new CMSMessageSignatureServiceException(
                        CMSMessageSignatureServiceException.SE_FAILURE);
            }

            data = Utils.stringToBytes(string);
        }

        int slotCount = APDUManager.getSlotCount();

        while (true) {

            for (int i = 0; i < slotCount; i++) {

                WIMApplication w = WIMApplication.getInstance(
                                     classSecurityToken, i, null, true);
                if (w == null) {
                    continue;
                }
                try {
                    return w.generateSignature(action == SIGN_STRING,
                                                  data, options, names);
                } finally {
                    w.done();
                }
            }
            // WIM application is not found

            if (securityElementPrompt != null) {
                try {
                    if (MessageDialog.showMessage(classSecurityToken,
                        Resource.getString(ResourceConstants
					   .JSR177_WIM_NOT_FOUND),
                        securityElementPrompt, true) != -1) {
                        continue;
                    }
                } catch (InterruptedException e) {}
            }
            throw new UserCredentialManagerException(
                          UserCredentialManagerException.SE_NOT_FOUND);
        }
    }

    /**
     * Loads the list of key identifiers for which certificates are
     * expected.
     * @return the list
     */
    private static Vector loadCSRList() {

        if (! persistentCSRList()) {
            if (tmpCSRList == null) {
                tmpCSRList = new Vector();
            }
            return tmpCSRList;
        }

        Vector CSRs = new Vector();

    	String storeName = File.getStorageRoot(
	    Constants.INTERNAL_STORAGE_ID) + CSR_ID_FILE;
    	RandomAccessStream storage =
                new RandomAccessStream(classSecurityToken);
        DataInputStream dis;

        try {
            storage.connect(storeName, Connector.READ);
            dis = new DataInputStream(storage.openInputStream());
        } catch (IOException ioe) {

            try {
                storage.connect(storeName, Connector.READ_WRITE);
                DataOutputStream dos = storage.openDataOutputStream();
                dos.writeInt(0);
                dos.flush();
                dos.close();
                dis = new DataInputStream(storage.openInputStream());
            } catch (IOException openwe) {
                return CSRs;
            }
            try {
                storage.disconnect();
            } catch (IOException e) {} // ignored
            return CSRs;
        }

        try {
            int count = dis.readInt();
            while (count-- > 0) {
                byte[] id = new byte[20];
                dis.read(id, 0, 20);
                CSRs.addElement(id);
            }
        } catch (IOException e) {} // ignored
        finally {
            try {
                storage.disconnect();
            } catch (IOException e) {} // ignored
        }
        return CSRs;
    }

    /**
     * Stores the list of key identifiers for which certificates are
     * expected.
     * @param CSRs the list
     */
    private static void storeCSRList(Vector CSRs) {

        if (! persistentCSRList()) {
            return;
        }

        String storeName = File.getStorageRoot(
	    Constants.INTERNAL_STORAGE_ID) + CSR_ID_FILE;
        RandomAccessStream storage =
                new RandomAccessStream(classSecurityToken);
        DataOutputStream dos;

        try {
            storage.connect(storeName, Connector.WRITE);
            dos = storage.openDataOutputStream();
            int len = CSRs.size();
            dos.writeInt(len);
            for (int i = 0; i < len; i++) {
                dos.write((byte[]) CSRs.elementAt(i));
            }
            dos.flush();
            dos.close();
            storage.truncate(4 + len * 20);
        } catch (IOException openwe) {} // ignored
        finally {
            try {
                storage.disconnect();
            } catch (IOException e) {} // ignored
        }
    }

    /**
     * Returns true if the list of generated CSR IDs must be stored
     * in persistent storage.
     * @return true if the list of generated CSR IDs must be stored
     * in persistent storage
     */
    private static boolean persistentCSRList() {
        return "true".equals(
                Configuration.getProperty("com.sun.satsa.store_csr_list"));
    }
}
