/*
 * 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 http://www.netbeans.org/cddl.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.netbeans.modules.autoupdate;

import java.io.*;
import java.security.*;
import java.security.cert.*;
import java.security.cert.Certificate;
import java.util.*;
import java.util.jar.*;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.progress.ProgressHandleFactory;
import org.netbeans.progress.module.InternalHandle;
import org.openide.ErrorManager;

import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;




/** Class for verifying signs in NBM Files.
 * @author  Martin Ryzl, Petr Hrebejk
 */
class SignVerifier extends Object {

    /** The password to the Forte keystore */
    private static final String KS_PSSWD = "open4all"; // NOI18N

    private static final String ENTRY_SEPARATOR = "/";  // NOI18N
    private static final String NBM_MAIN = "main"; // NOI18N
    private static final String NBM_MODULES = "netbeans/modules"; // NOI18N
    private static final String NBM_AUTOLOAD = NBM_MODULES + ENTRY_SEPARATOR + SafeModule.PROP_AUTOLOAD; // NOI18N
    private static final String NBM_EAGER = NBM_MODULES + ENTRY_SEPARATOR + SafeModule.PROP_EAGER; // NOI18N
    private static final String JAR_EXT = ".jar";

    public static final int NOT_CHECKED = -1;
    public static final int BAD_DOWNLOAD = 0;
    public static final int CORRUPTED = 1;
    public static final int NOT_SIGNED = 2;
    public static final int SIGNED = 3;
    public static final int TRUSTED = 4;

    private static final String NEW_LINE = "\n"; // NOI18N
    private static final String SPACE = " "; // NOI18N
    private static final String TAB = "\t"; //NOI18N

    /** The update check progress panel */
    DownloadProgressPanel progressDialog;
    /** Wizard validator, enables the Next button in wizard */
    private Wizard.Validator validator;
    /** Total size of the verify */
    private int verifySize;
    /** KBytes verified */
    private int totalVerified;
    /** Number of modules to verify */
    private long modulesCount;

    private boolean verifyCanceled = false;
    private boolean wizardCanceled = false;
    
    // progress
    ProgressHandle partialHandle;
    ProgressHandle overallHandle;
    long startTime;
    
    private static final ErrorManager err = ErrorManager.getDefault ().getInstance ("org.netbeans.modules.autoupdate"); // NOI18N

    /** Creates new VeriSign */
    SignVerifier (DownloadProgressPanel progressDialog, Wizard.Validator validator) {
        this.validator = validator;
        this.progressDialog = progressDialog;
    }


    /** Verifies all downloaded modules */
    void doVerify() {
        verifyCanceled = false;
        progressDialog.setPartialLabel (getBundle( "CTL_PreparingVerify_Label")); // NOI18N

        verifySize = getTotalVerifySize();
        err.log ("Runing doVerify, check the total size " + verifySize + ", verifyCanceled? " + verifyCanceled);

        if ( verifyCanceled )
            return;

        verifyAll();

        err.log ("Verification done.");
        if ( verifyCanceled )
            return;

        validator.setValid( true );
    }


    /** Total size for verify in KBytes */
    private int getTotalVerifySize( ) {
        int result = 0;
        modulesCount = 0;

        Iterator it = Wizard.getAllModules().iterator();

        while( it.hasNext() ) {
            ModuleUpdate mu = (ModuleUpdate)it.next();

            if ( mu.isSelected() && mu.isDownloadOK() && 
                    (mu.getSecurity() == NOT_CHECKED || mu.getSecurity() == BAD_DOWNLOAD )) {
                File file = Downloader.getNBM( mu );
                result += file.length();
                modulesCount++;
            }
        }
        return result;
    }


    void verifyAll() {

        overallHandle = getOverallHandle (verifySize);

        progressDialog.setPartialLabel (""); // NOI18N
        progressDialog.setOverallLabel (""); // NOI18N
        progressDialog.setExtraLabel (""); // NOI18N
        
        int currentModule = 0;
        totalVerified = 0;

        Iterator it = Wizard.getAllModules().iterator();

        while( it.hasNext() ) {

            if ( verifyCanceled )
                return;

            ModuleUpdate mu = (ModuleUpdate)it.next();
            if ( mu.isSelected() && mu.isDownloadOK() && 
                    (mu.getSecurity() == NOT_CHECKED || mu.getSecurity() == BAD_DOWNLOAD )) {

                if ( verifyCanceled )
                    return;

                progressDialog.setPartialLabel (mu.getName() + " [" + (currentModule + 1) + "/" + modulesCount + "]" ); //NOI18N

                File file = Downloader.getNBM( mu );
                try {
                    Collection certificates = verifyJar( file, mu );

                    if ( certificates == null ) {
                        err.log (mu.getName () + " was verified as NOT_SIGNED");

                        mu.setSecurity( NOT_SIGNED );
                        mu.setInstallApproved( false );
                    }
                    else {
                        mu.setCerts( certificates );

                        if ( isTrusted( certificates ) ) {
                            err.log (mu.getName () + " was verified as TRUSTED");
                            mu.setSecurity( TRUSTED );
                            mu.setInstallApproved( true );
                        }
                        else {
                            err.log (mu.getName () + " was verified as SIGNED");
                            mu.setSecurity( SIGNED );
                            mu.setInstallApproved( false );
                        }
                    }
                }
                catch( SecurityException e ) {
                    err.log (mu.getName () + " was verified as CORRUPTED");
                    mu.setSecurity( CORRUPTED );
                    mu.setInstallApproved( false );
                }
                catch( IOException e ) {
                    err.log (mu.getName () + " was verified as BAD_DOWNLOAD");
                    mu.setSecurity( BAD_DOWNLOAD );
                    mu.setInstallApproved( false );
                    mu.setDownloadOK( false );
                }

                currentModule++;
                overallHandle.progress (totalVerified);
            }
        }
        
        overallHandle.finish ();
        String mssgTotal = NbBundle.getMessage( SignVerifier.class, "FMT_VerifiedTotal", // NOT_SIGNED
                           new Object[] { new Integer( verifySize / 1024 ),
                                          new Integer( verifySize / 1024 ) } );
        progressDialog.setOverallLabel (mssgTotal);
    }


    /**
    * @param args the command line arguments
    */
    /*
    public void check ( File file ) throws Exception {
      Collection certificates = verifyJar( file );
      
      showCollection(certificates);
      KeyStore keyStore = getKeyStore(KEYSTORE, "changeit", null);
      showCollection(getCertificates(keyStore));  
}
    */
    
    static void processJarEntry (JarEntry entry, ModuleUpdate mu) {
        // dangerous modules should be updated into install directory
        if ( entry.getName().startsWith( "netbeans/" + ModuleUpdate.NBM_LIB )) { // NOI18N
            mu.setDepending( true );
        }

        if ( entry.getName().startsWith( NBM_MAIN ) )
            mu.setSafeToInstall( false );

        String jarname = null;
        if ( entry.getName().startsWith( NBM_AUTOLOAD ) && entry.getName().endsWith( JAR_EXT ) ) {
            jarname = getJarModuleName( entry.getName(), NBM_AUTOLOAD + ENTRY_SEPARATOR );
            if ( jarname != null )
                mu.addToJarList( jarname );
        }
        else if ( entry.getName().startsWith( NBM_EAGER ) && entry.getName().endsWith( JAR_EXT ) ) {
            jarname = getJarModuleName( entry.getName(), NBM_EAGER + ENTRY_SEPARATOR );
            if ( jarname != null )
                mu.addToJarList( jarname );
        }
        else if( entry.getName().startsWith( NBM_MODULES ) && entry.getName().endsWith( JAR_EXT ) ) {
            jarname = getJarModuleName( entry.getName(), NBM_MODULES + ENTRY_SEPARATOR );
            if ( jarname != null )
                mu.addToJarList( jarname );
        }
    }

    /** Verify jar file.
    * @return Collection of certificates
    */
    private Collection verifyJar( File jarName, ModuleUpdate mu ) throws SecurityException, IOException {

        JarInputStream jis = null;
        boolean anySigned = false;
        boolean anyUnsigned = false;
        int moduleVerified = 0;

        int flen = (int) jarName.length();

        partialHandle = getPartialHandle (flen);

        JarFile jf = new JarFile(jarName);
        Manifest man = jf.getManifest();
        
        Enumeration entries = jf.entries();
        LinkedList entriesList = new LinkedList();
        byte[] buffer = new byte[8192];
        
        try {
            while (entries.hasMoreElements()) {
                JarEntry entry = (JarEntry)entries.nextElement();
                
                processJarEntry (entry, mu);

                entriesList.add(entry);
                InputStream is = jf.getInputStream(entry);
                try {
                    int n;
                    while ((n = is.read(buffer, 0, buffer.length)) != -1) {
                        // we just read. this will throw a SecurityException
                        moduleVerified ++;
                        if ( moduleVerified % 4096 == 0 ) {
                            partialHandle.progress (moduleVerified < flen ? moduleVerified : flen);
                        }
                        // if  a signature/digest check fails.
                        if ( verifyCanceled )
                                return null;
                    }                
                } finally {
                    is.close();
                    totalVerified += entry.getCompressedSize();

                    String mssgTotal = NbBundle.getMessage( SignVerifier.class, "FMT_VerifiedTotal",
                                       new Object[] { new Integer( totalVerified / 1024 ),
                                                      new Integer( verifySize / 1024 ) } );
                    progressDialog.setOverallLabel (mssgTotal);
                }
            }
            
            err.log ("Update " + mu.getCodeNameBase () + " was verified as forced to intall global: " + mu.isForcedGlobal () + " and safeToInstall: " + mu.isSafeToInstall ());
            
            if ( verifyCanceled )
                return null;
        } finally {
            jf.close();
            if ( wizardCanceled )
                Downloader.getNBM( mu ).delete();
        }        

        partialHandle.finish ();
        
        Set certificates = new HashSet();
        if (man != null) {
            Iterator e = entriesList.iterator();
            while (e.hasNext()) {
                JarEntry je = (JarEntry) e.next();
                String name = je.getName();
                Certificate[] certs = je.getCertificates();
                boolean isSigned = ((certs != null) && (certs.length > 0));
                anySigned |= isSigned;
                if (certs != null) {
                    for(int i = 0; i < certs.length; i++) {
                        certificates.add(certs[i]);
                        if ( verifyCanceled )
                            return null;
                    }
                }
                else { // The entry is not signed
                    if ( !je.isDirectory() && !name.toUpperCase().startsWith( "META-INF/" )   // NOI18N
                            && je.getSize() != 0 ) {
                        anyUnsigned = true;
                    }
                }
            }
        }

        if ( anySigned && anyUnsigned ) {
            throw new SecurityException( getBundle( "EXC_NotSignedEntity" ) );
        }

        return anySigned ? certificates : null;
    }

    public static String formatCerts(Collection collection) {
        StringBuffer sb = new StringBuffer( collection.size() * 300 );


        Iterator it = collection.iterator();
        while(it.hasNext()) {
            Certificate cert = (Certificate)it.next();

            if ( cert instanceof X509Certificate ) {
                try {
                    sb.append( "\n\n" ); // NOI18N
                    sb.append( X509CertToString( (X509Certificate) cert ) );
                }
                catch ( Exception e ) {
                    sb.append( cert.toString() );
                }
            }
            else {
                sb.append( cert.toString() );
            }
            sb.append( "\n\n" ); // NOI18N
        }

        return sb.toString();
    }

    /** Tests whether the cets are trusted
     */
    boolean isTrusted( Collection certs ) {

        Collection trustedCerts = getTrustedCerts();

        if ( trustedCerts.size() <= 0 || certs.size() <= 0 )
            return false;

        return trustedCerts.containsAll( certs );
    }

    /** Adds certificates into keystore
    */

    static void addCertificates( Collection certs )
    throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException {

        KeyStore ks = getKeyStore( Autoupdater.Support.getKSFile (), KS_PSSWD, null);

        Iterator it = certs.iterator();

        while ( it.hasNext() ) {
            Certificate c = (Certificate) it.next();
            
            // don't add certificate twice
            if ( ks.getCertificateAlias( c ) != null )
                continue;

            // Find free alias name
            String alias = null;
            for ( int i = 0; i < 9999; i++ ) {
                alias = "genAlias" + i; // NOI18N
                if ( !ks.containsAlias( alias ) )
                    break;
            }
            if ( alias == null )
                throw new KeyStoreException( getBundle( "EXC_TooManyCertificates" ) );

            ks.setCertificateEntry( alias, c ) ;
        }


        saveKeyStore( ks, Autoupdater.Support.getKSFile (), KS_PSSWD, null);
    }

    /** Removes certificates from the keystore */
    static void removeCertificates( Collection certs )
    throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException {

        KeyStore ks = getKeyStore( Autoupdater.Support.getKSFile (), KS_PSSWD, null);

        Iterator it = certs.iterator();

        while ( it.hasNext() ) {
            Certificate c = (Certificate) it.next();

            String alias = ks.getCertificateAlias( c );

            if ( alias != null )
                ks.deleteEntry( alias );

        }

        saveKeyStore( ks, Autoupdater.Support.getKSFile (), KS_PSSWD, null);

    }


    // Keystore utility methods --------------------------------------------------------


    /** Gets all trusted certificates */
    Collection getTrustedCerts() {

        Collection trustedCerts = new ArrayList( 10 );

        File ksFile = Autoupdater.Support.getKSFile ();

        try {
            if ( ksFile.canRead() ) {
                KeyStore ks = getKeyStore (ksFile, KS_PSSWD, null);
                trustedCerts.addAll ( getCertificates( ks ) );
            }
        }
        // In case of exception let the collection empty
        catch ( CertificateException  e ) {
        }
        catch ( KeyStoreException e ) {
        }
        catch ( IOException e ) {
        }
        catch ( NoSuchAlgorithmException e ) {
        }

        return trustedCerts;
    }

    /** Creates keystore and loads data from file.
    * @param filename - name of the keystore
    * @param storetype - type of the keystore
    * @param password
    */
    private static KeyStore getKeyStore(File file, String password, String storetype)
    throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException {
        if (file == null) return null;

        InputStream is = null;

        try {
            is = new FileInputStream( file );
        }
        catch ( IOException e ) {
            // Do nothing leaving is null creates empty key store
        }

        KeyStore keyStore = null;

        if ( storetype == null ) {
            keyStore = KeyStore.getInstance( KeyStore.getDefaultType() );
            keyStore.load( is, password.toCharArray() );
            if ( is != null )
                is.close();
        }

        return keyStore;
    }

    /** Creates keystore and loads data from file.
    * @param keyStore - keystore
    * @param filename - name of the keystore
    * @param storetype - type of the keystore
    * @param password
    */
    public static void saveKeyStore(KeyStore keyStore, File file, String password, String storetype)
    throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException {
        
        if (file == null) return ;
        
        OutputStream os = new FileOutputStream( file );
        keyStore.store(os, password.toCharArray( ) );
        os.close();
    }

    /** Return all certificates in system ( Form central and user's directory )
    */
    public static Collection getCertificates(KeyStore keyStore) throws KeyStoreException {

        List certificates = new ArrayList( 10 );

        Enumeration en = keyStore.aliases();
        while (en.hasMoreElements()) {
            String alias = (String)en.nextElement();
            Certificate cert = keyStore.getCertificate(alias);
            certificates.add(cert);
        }
        return certificates;
    }

    void cancelVerify(boolean wizardCanceled) {
        verifyCanceled = true;
        this.wizardCanceled = wizardCanceled;
    }


    /** Prints a X509 certificate in a human readable format.
    */
    private static String X509CertToString(X509Certificate cert )
    throws Exception
    {
        return getBundle("MSG_Owner") + SPACE + (cert.getSubjectDN()) + NEW_LINE +
               getBundle("MSG_Issuer") + SPACE + (cert.getIssuerDN()) + NEW_LINE +
               getBundle("MSG_SerNumber") + SPACE + cert.getSerialNumber().toString(16) + NEW_LINE +
               getBundle("MSG_Valid") + SPACE + cert.getNotBefore().toString() +
               SPACE + getBundle("MSG_Until") + SPACE + cert.getNotAfter().toString() + NEW_LINE +
               getBundle("MSG_CertFinger") + NEW_LINE +
               SPACE + TAB + getBundle("MSG_MD5") + SPACE + SPACE + getCertFingerPrint("MD5", cert) + NEW_LINE +
               SPACE + TAB + getBundle("MSG_SHA1") + SPACE + getCertFingerPrint("SHA1", cert);
    }

    /** Gets the requested finger print of the certificate.
    */
    private static String getCertFingerPrint(String mdAlg, Certificate cert)
    throws Exception
    {
        byte[] encCertInfo = cert.getEncoded();
        MessageDigest md = MessageDigest.getInstance(mdAlg);
        byte[] digest = md.digest(encCertInfo);
        return toHexString(digest);
    }

    /**Converts a byte array to hex string
     */
    private static String toHexString(byte[] block) {
        StringBuffer buf = new StringBuffer();
        int len = block.length;
        for (int i = 0; i < len; i++) {
            byte2hex(block[i], buf);
            if (i < len-1) {
                buf.append(":"); // NOI18N
            }
        }
        return buf.toString();
    }


    /** Converts a byte to hex digit and writes to the supplied buffer
    */
    private static void byte2hex(byte b, StringBuffer buf) {
        char[] hexChars = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
                            '9', 'A', 'B', 'C', 'D', 'E', 'F' };
        int high = ((b & 0xf0) >> 4);
        int low = (b & 0x0f);
        buf.append(hexChars[high]);
        buf.append(hexChars[low]);
    }
    
    private static String getJarModuleName( String name, String prefix ) {
        if ( name.substring( prefix.length() ).indexOf( ENTRY_SEPARATOR ) == -1 ) {
            String common = NBM_MODULES + ENTRY_SEPARATOR;
            return name.substring( common.length() );
        }
        return null;
    }

    private static String getBundle( String key ) {
        return NbBundle.getMessage( SignVerifier.class, key );
    }

    private ProgressHandle getPartialHandle (int units) {
        assert progressDialog != null;
        ProgressHandle handle = ProgressHandleFactory.createHandle (getBundle ("DownloadProgressPanel_partialHandle_name")); // NOI18N
        progressDialog.setPartialProgressComponent (handle);
        handle.start (units);
        return handle;
    }
    
    private ProgressHandle getOverallHandle (int units) {
        assert progressDialog != null;
        final ProgressHandle handle = ProgressHandleFactory.createHandle (getBundle ("DownloadProgressPanel_overallHandle_name")); // NOI18N
        progressDialog.setOverallProgressComponent (handle);
        handle.start (units);
        return handle;
    }

}
