/*
 *   
 *
 * 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.midp.jadtool;

import java.io.*;
import java.util.*;
import java.security.cert.X509Certificate;

/**
 * The JadTool is a command line interface to the AppDescriptor class.
 *
 * @see AppDescriptor
 * @see SignCert
 * @see java.security.KeyStore
 */
public class JadTool {
    /** Usage text. */
    private static String USAGE = 
        "\nJadTool arguments:\n" +
        "-help\n" +
        "-addcert\n" +
        "\t-alias <key alias> [-storepass <password>] " +
        "[-keystore <keystore>]\n" +
        "\t[-certnum <number>] [-chainnum <number>]\n" +
        "\t[-encoding <encoding>] -inputjad <filename> " +
        "-outputjad <filename>\n" +
        "-addjarsig\n" +
        "\t[-jarfile <filename>] -keypass <password> -alias <key alias>\n" +
        "\t-storepass <password> [-keystore <keystore>] " +
        "[-encoding <encoding>]\n" +
        "\t-inputjad <filename> -outputjad <filename>\n" +
        "-showcert\n" +
        "\t[([-certnum <number>] [-chainnum <number>]) | [-all]]\n" +
        "\t[-encoding <encoding>] -inputjad <filename>\n" +
        "\n" +
        "The default for -encoding is UTF-8.\n" +
        "The default for -jarfile is the MIDlet-Jar-URL " +
        "property in the JAD.\n" +
        "The default for -keystore is \"$HOME/.keystore\".\n" +
        "The default for -certnum is 1.\n" +
        "The default for -chainnum is 1.\n";

    /** Holds the command given on the command line. */
    private String command = null;
    /** Holds the JAD encoding given on the command line. */
    private String encoding = null;
    /** Holds the input JAD filename given on the command line. */
    private String infile = null;
    /** Holds the output JAD filename given on the command line. */
    private String outfile = null;
    /** Holds the JAR filename given on the command line. */
    private String jarfile = null;
    /** Holds the keystore filename given on the command line. */
    private String keystore = null;
    /** Holds the key alias given on the command line. */
    private String alias = null;
    /** Holds the certificate number given on the command line. */
    private String certnum = null;
    /** Holds the certificate chain number given on the command line. */
    private String chainNum = null;
    /** Holds the keystore password given on the command line. */
    private char[] storepass = null;
    /** Holds the private key password given on the command line. */
    private char[] keypass = null;

    /** The converted certificate number. */
    private int certIndex = 1;
    /** The converted certificate chain number. */
    private int chainIndex = 1;
    /** The in-memory JAD. */
    private AppDescriptor appdesc = null;
    /** Stream to the output JAD. */
    private OutputStream outstream;

    /** Keeps this class from being instantiated by the public. */
    private JadTool() { }

    /**
     * Performs the command specified in the command line arguments.
     * <p>
     * Exits with a 0 status if the command was successful.
     * Exits and prints out an error message with a -1 status if the command
     * failed.</p>
     * <pre>
     * Command Summary
     * ---------------
     *
     * -help         - print a usage summary
     *
     * -addjarsig    - add a SHA1withRSA PKCS v1.5 signature of a jarfile to an
     *                 app descriptor file.
     *
     * -addcert      - add a https or content provider certificate
     *                 to an app descriptor file.
     *
     * -showcert     - show a certificate from the app descriptor 
     *                 file in human readable form.
     *
     * Options 
     * -------
     *
     * These options are valid (and in some cases required to be use) with
     * the commands above.
     *
     * -inputjad      <original app descriptor file> 
     *
     * -outputjad     <output app descriptor file, can be the same as infile> 
     *
     * -encoding   <encoding type> 
     *             (default is UTF-8)
     *
     * -jarfile    <jar file>
     *
     * -keystore   <keystore file to use>
     *             (default is .keystore - same as with keytool)
     *
     * -storepass  <password to unlock chosen keystore>
     *
     * -alias      <alias of  key or certificate in keystore>
     *
     * -keypass    <password to unlock signing key>
     *
     * -certnum    <number> (default is "1" or calculated for addcert)
     *
     * -chainnum   <number> (default is "1")
     *
     * -all        show all certificates
     *
     *
     * Command Descriptions
     * --------------------
     * help:          Shows a list of use options.
     *
     * addcert:       Given a certificate and an unsigned jad file
     *                JadTool will create a base64 encoding of the 
     *                certificate file and include it in the jad file as  
     *
     *                   MIDlet-Certificate-1-1: <base64 string> 
     *                                
     *                "base64 string" is a base64 encoding of the certificate. 
     *                If the certificate already exists in the jad file
     *                it will not be duplicated.
     *
     * showcert:      Usage: java JadTool -showcert -certnum <num>
     *                Allows choice of certificate to view using this command.
     *
     * addjarsig:     Given the alias of a key (stored by the J2SE "keytool"
     *                utility" and an unsigned jad file, the JadTool will
     *                check to see that the jad file contains a 
     *                a  MIDlet-Certificate-1-1
     *                field.  It will then sign the jar file.
     *                In the event that no target jar file is specified
     *                JadTool will hash the jar file located at the URL 
     *                specified by the MIDlet-Jar-URL property if no 
     *                explicit jar file is given.
     *</pre>
     *
     * @param args command line arguments given by user.
     */
    public static void main(String[] args) {
        int exitStatus = -1;

        try {
            new JadTool().run(args);
            exitStatus = 0;
        } catch (Exception e) {
            System.err.println("\n" + e.getMessage() + "\n");
        }

        System.exit(exitStatus);
    }
    
    /**
     * Parses the command line arguments and runs the selected command. 
     * <p>
     * Does not return.
     * Exits with a 0 status if the command was successful.
     * Exits and prints out an error message with a -1 status if the command
     * failed.</p>
     *
     * @param args The command line arguments given to main.
     *
     * @exception Exception if there are any errors
     */
    private void run(String [] args) throws Exception {
        if (args.length == 0) {
            usageError("No command given");
        }

        command = args[0];

        try {
            if (command.equals("-addjarsig")) {
                performAddJarSigCommand(args);
                return;
            }

            if (command.equals("-addcert")) {
                performAddCertCommand(args);
                return;
            }

            if (command.equals("-showcert")) {
                performShowCertCommand(args);
                return;
            }

            if (command.equals("-help")) {
                for (int i = 1; i < args.length; i++) {
                    usageError("Illegal option for " + command + ": " +
                               args[i]);
                }

                // help exits
                help();
            }

            usageError("Illegal command: " + command);
        } finally {
            // zero-out passwords
            if (storepass != null) {
                Arrays.fill(storepass, ' ');
                storepass = null;
            }

            if (keypass != null) {
                Arrays.fill(keypass, ' ');
                keypass = null;
            }

            try {
                if (outstream != null) {
                    outstream.close();
                }
            } catch (IOException ioe) {
                // do nothing.  
            }
        }
    }
    
    /**
     * Perform the -addjarsig command, including parsing the line arguments
     * for the -addjarsig command.
     * <p>
     * If there is a problem parsing an argument, print the error,
     * print the usage, and exit with a -1.
     *
     * @param args The command line arguments given to main.
     *
     * @exception Exception if there are any errors
     */
    private void performAddJarSigCommand(String[] args) throws Exception {
        int i = 1;

        try {
            for (i = 1; i < args.length; i++) {

                if (args[i].equals("-encoding")) {
                    encoding = args[++i];
                } else if (args[i].equals("-keystore")) {
                    keystore = args[++i];
                } else if (args[i].equals("-storepass")) {
                    storepass = args[++i].toCharArray();
                } else if (args[i].equals("-keypass")) {
                    keypass = args[++i].toCharArray();
                } else if (args[i].equals("-alias")) {
                    alias = args[++i];
                } else if (args[i].equals("-jarfile")) {
                    jarfile = args[++i];
                } else if (args[i].equals("-inputjad")) {
                    infile = args[++i];
                } else if (args[i].equals("-outputjad")) {
                    outfile = args[++i];
                } else {
                    usageError("Illegal option for " + command +
                               ": " + args[i]);
                }
            }
        } catch (ArrayIndexOutOfBoundsException aiobe) {
            usageError("Missing value for " + args[--i]);
        }

        if (keypass == null) {
            usageError(command + " requires -keypass");
        }

        // these methods will check for the presence of the args they need
        initJadUtil();
        openKeystoreAndOutputJad();

        if (jarfile != null) {
            // a jar file was specified for use
            FileInputStream jarinput;

            try {
                jarinput = new FileInputStream(jarfile);
            } catch (FileNotFoundException fnfe) {
                throw new Exception("JAR does not exist: " + jarfile);
            }

            try {
                appdesc.addJarSignature(alias, keypass, jarinput);
            } catch (Exception e) {
                throw new Exception(command + " failed: " + e.toString());
            }

            try {
                jarinput.close();
            } catch (Exception e) {
                // ignore
            }
        } else {
            // Use the JAR at MIDlet-Jar-URL in the JAD
            try {
                appdesc.addJarSignature(alias, keypass);
            } catch (Exception e) {
                throw new Exception(command + " failed: " + e.toString());
            }
        }

        appdesc.store(outstream, encoding);
        return;
    }

    /**
     * Perform the -addcert command, including parsing the line arguments
     * for the -addcert command.
     * <p>
     * If there is a problem parsing an argument, print the error,
     * print the usage, and exit with a -1.
     *
     * @param args The command line arguments given to main.
     *
     * @exception Exception if there are any errors
     */
    private void performAddCertCommand(String[] args) throws Exception {
        int i = 1;

        // change the default for cert number for this command
        certIndex = 0;

        try {
            for (i = 1; i < args.length; i++) {

                if (args[i].equals("-encoding")) {
                    encoding = args[++i];
                } else if (args[i].equals("-keystore")) {
                    keystore = args[++i];
                } else if (args[i].equals("-storepass")) {
                    storepass = args[++i].toCharArray();
                } else if (args[i].equals("-alias")) {
                    alias = args[++i];
                } else if (args[i].equals("-certnum")) {
                    certnum = args[++i];
                } else if (args[i].equals("-chainnum")) {
                    chainNum = args[++i];
                } else if (args[i].equals("-inputjad")) {
                    infile = args[++i];
                } else if (args[i].equals("-outputjad")) {
                    outfile = args[++i];
                } else  {
                    usageError("Illegal option for " + command +
                               ": " + args[i]);
                }
            }
        } catch (ArrayIndexOutOfBoundsException aiobe) {
            usageError("Missing value for " + args[--i]);
        }

        // these methods will check for the presence of the args they need
        checkCertAndChainNum();
        initJadUtil();
        openKeystoreAndOutputJad();

        try {
            appdesc.addCert(alias, chainIndex, certIndex);
            appdesc.store(outstream, encoding);
            return;
        } catch (Exception e) {
            throw new Exception(command + " failed: " + e.toString());
        }
    }

    /**
     * Perform the -showcert command, including parsing the line arguments
     * for the -showcert command.
     * <p>
     * If there is a problem parsing an argument, print the error,
     * print the usage, and exit with a -1.
     *
     * @param args The command line arguments given to main.
     *
     * @exception Exception if there are any errors
     */
    private void performShowCertCommand(String[] args) throws Exception {
        int i = 1;
        X509Certificate c;
        boolean listAll = false;

        try {
            for (i = 1; i < args.length; i++) {

                if (args[i].equals("-encoding")) {
                    encoding = args[++i];
                } else if (args[i].equals("-certnum")) {
                    certnum = args[++i];
                } else if (args[i].equals("-chainnum")) {
                    chainNum = args[++i];
                } else if (args[i].equals("-all")) {
                    listAll = true;
                } else if (args[i].equals("-inputjad")) {
                    infile = args[++i];
                } else {
                    usageError("Illegal option for " + command +
                               ": " + args[i]);
                }
            }
        } catch (ArrayIndexOutOfBoundsException aiobe) {
            usageError("Missing value for " + args[--i]);
        }

        if (listAll && (chainNum != null || certnum != null)) {
            usageError("-all cannot be used with -certnum or -chainnum");
        }

        // these methods will check for the presence of the args they need
        checkCertAndChainNum();
        initJadUtil();

        if (listAll) {
            Vector certs = appdesc.getAllCerts();

            if (certs.size() == 0) {
                System.out.println("\nNo certificates found in JAD.\n");
                return;
            }

            System.out.println();

            for (i = 0; i < certs.size(); i++) {
                Object[] temp = (Object[])certs.elementAt(i);

                System.out.println((String)temp[AppDescriptor.KEY] + ":");

                displayCert((X509Certificate)temp[AppDescriptor.CERT]);
            }

            return;
        }

        try {
            c = appdesc.getCert(chainIndex, certIndex);
        } catch (Exception e) {
            throw new Exception("-showcert failed: " + e.toString());
        }

        if (c == null) {
            throw new Exception("Certificate " + chainIndex + "-" +
                                certIndex + " not in JAD");
        }

        try {
            displayCert(c);
            return;
        } catch (Exception e) {
            throw new Exception("-showcert failed: " + e.toString());
        }
    }

    /**
     * Check the format of the certificate and chain numbers. If there is a
     * problem, print the error, print usage, and exit with -1.
     */
    private void checkCertAndChainNum() {
        if (certnum != null) {
            try {
                certIndex = (Integer.valueOf(certnum)).intValue();
                if (certIndex <= 0) {
                    usageError("-certnum must be a positive number");
                }
            } catch (NumberFormatException nfe) {
                usageError("-certnum must be a positive number");
            }
        } 

        if (chainNum != null) {
            try {
                chainIndex = (Integer.valueOf(chainNum)).intValue();
                if (chainIndex <= 0) {
                    usageError("-chainnum must be a positive number");
                }
            } catch (NumberFormatException nfe) {
                usageError("-chainnum must be a positive number");
            }
        }
    }

    /**
     * Initializes an instance of the AppDescriptor class.
     * <p>
     * If the input file has not been specified, print an error and the usage,
     * then exit with a -1
     *
     * @exception Exception if there are any errors
     */
    private void initJadUtil() throws Exception {
        InputStream instream;

        if (infile == null) {
            usageError(command + " requires an input JAD");
        }

        try {
            FileInputStream fis = new FileInputStream(infile);
            instream = new BufferedInputStream(fis);
        } catch (FileNotFoundException fnfe) {
            throw new Exception("Input JAD does not exist: " + infile);
        }

        try {
            appdesc = new AppDescriptor();
            appdesc.load(instream, encoding);
        } catch (UnsupportedEncodingException uee) {
            throw new Exception("Encoding type " + encoding +
                                " not supported");
        } catch (IOException ioe) {
            throw new Exception("Error parsing input JAD: " + infile);
        } finally {
            try {
                // close now so the input and output JAD can be the same.
                instream.close();
            } catch (Exception e) {
                // ignore
            }
        }
    }

    /**
     * Open the keystore and output JAD file.
     * <p>
     * If the key alias or output file has not been specified, print an
     * error and the usage, then exit with a -1
     *
     * @exception Exception if there are any errors
     */
    private void openKeystoreAndOutputJad() throws Exception {
        File ksfile;
        FileInputStream ksstream;

        if (alias == null) {
            usageError(command + " requires -alias");
        }

        if (outfile == null) {
            usageError(command + " requires an output JAD");
        }

        if (keystore == null) {
            keystore = System.getProperty("user.home") + File.separator
                       + ".keystore";
        }

        try {
            ksfile = new File(keystore);
            // Check if keystore file is empty
            if (ksfile.exists() && ksfile.length() == 0) {
                throw new Exception("Keystore exists, but is empty: " +
                                    keystore);
            }

            ksstream = new FileInputStream(ksfile);
        } catch (FileNotFoundException fnfe) {
            throw new Exception("Keystore does not exist: " + keystore);
        }

        try {
            try {
                // the stream will be closed later
                outstream = new FileOutputStream(outfile);
            } catch (IOException ioe) {
                throw new Exception("Error opening output JAD: " +
                                    outfile);
            }

            try {
                // load the keystore into the AppDescriptor
                appdesc.loadKeyStore(ksstream, storepass);
            } catch (Exception e) {
                throw new Exception("Keystore could not be loaded: " +
                                    e.toString());
            }
        } finally {
            try {
                ksstream.close();
            } catch (IOException e) {
                // ignore
            }
        }
    }
    
    /**
     * Prints the usage cases of this tool and exits with 0 status.
     */
    private void help() {
        usage(0);
    }

    /**
     * Prints the usage cases of this tool and exits with -1 status.
     *
     * @param error usage error message to print
     */
    private void usageError(String error) {
        System.err.println("\n" + error);
        usage(-1);
    }

    /**
     * Prints the usage cases of this tool and exits.
     *
     * @param exitStatus status to exit with
     */
    private void usage(int exitStatus) {
        System.out.println(USAGE);
        System.exit(exitStatus);
    }

    /**
     * Display a certificate.
     *
     * @param c certificate to display
     *
     * @exception Exception if there are any errors
     */
    private void displayCert(X509Certificate c) throws Exception {
        String digest;

        System.out.println();

        System.out.println("Subject: " + c.getSubjectDN().getName());

        System.out.println("Issuer : " + c.getIssuerDN().getName());

        System.out.println("Serial number: " +
                               c.getSerialNumber().toString(16));

        System.out.println("Valid from " + c.getNotBefore() +
                           " to " + c.getNotAfter());
            
        System.out.println("Certificate fingerprints:");

        System.out.print("  MD5: ");
        digest = AppDescriptor.createFingerprint(c.getEncoded(), "MD5");
        System.out.println(digest);

        System.out.print("  SHA: ");
        digest = AppDescriptor.createFingerprint(c.getEncoded(), "SHA");
        System.out.println(digest);

        System.out.println();
    }
}
