/*
 * @(#)JarVerifier.java	1.35 03/12/19
 *
 * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package java.util.jar;

import java.io.*;
import java.util.*;
import java.util.zip.*;
import java.security.*;
import java.security.cert.CertificateException;

import sun.security.util.ManifestDigester;
import sun.security.util.ManifestEntryVerifier;
import sun.security.util.SignatureFileVerifier;
import sun.security.util.Debug;

/**
 *
 * @version 	1.35 03/12/19
 * @author	Roland Schemers
 */
class JarVerifier {

    /* Are we debugging ? */
    static final Debug debug = Debug.getInstance("jar");

    /* a table mapping names to code signers, for jar entries that have
       had their actual hashes verified */
    private Hashtable verifiedSigners;

    /* a table mapping names to code signers, for jar entries that have
       passed the .SF/.DSA -> MANIFEST check */
    private Hashtable sigFileSigners;

    /* a hash table to hold .SF bytes */
    private Hashtable sigFileData;

    /** "queue" of pending PKCS7 blocks that we couldn't parse
     *  until we parsed the .SF file */
    private ArrayList pendingBlocks;

    /* cache of CodeSigner objects */
    private ArrayList signerCache;

    /* Are we parsing a block? */
    private boolean parsingBlockOrSF = false;

    /* Are we done parsing META-INF entries? */
    private boolean parsingMeta = true;

    /* Are there are files to verify? */
    private boolean anyToVerify = true;

    /* The manifest file */
    private Manifest manifest;

    /* The output stream to use when keeping track of files we are interested
       in */
    private ByteArrayOutputStream baos;

    /** The ManifestDigester object */
    private ManifestDigester manDig;

    /** the bytes for the manDig object */
    byte manifestRawBytes[] = null;

    /**
     */
    public JarVerifier(Manifest manifest, byte rawBytes[]) {
	manifestRawBytes = rawBytes;
	sigFileSigners = new Hashtable();
	verifiedSigners = new Hashtable();
	sigFileData = new Hashtable(11);
	pendingBlocks = new ArrayList();
	baos = new ByteArrayOutputStream();
	this.manifest = manifest;
    }

    /**
     * This method scans to see which entry we're parsing and
     * keeps various state information depending on what type of
     * file is being parsed.
     */
    public void beginEntry(JarEntry je, ManifestEntryVerifier mev)
	throws IOException
    {
	if (je == null)
	    return;

	if (debug != null) {
	    debug.println("beginEntry "+je.getName());
	}

	String name = je.getName();

	/*
	 * Assumptions:
	 * 1. The manifest should be the first entry in the META-INF directory.
	 * 2. The .SF/.DSA files follow the manifest, before any normal entries
	 * 3. Any of the following will throw a SecurityException:
	 *    a. digest mismatch between a manifest section and
	 *       the SF section.
	 *    b. digest mismatch between the actual jar entry and the manifest
	 */

	if (parsingMeta) {
	    String uname = name.toUpperCase(Locale.ENGLISH);
	    if ((uname.startsWith("META-INF/") ||
		 uname.startsWith("/META-INF/"))) {

		if (je.isDirectory()) {
		    mev.setEntry(null, je);
		    return;
		}

		if (SignatureFileVerifier.isBlockOrSF(uname)) {
		    /* We parse only DSA or RSA PKCS7 blocks. */
		    parsingBlockOrSF = true;
		    baos.reset();
		    mev.setEntry(null, je);
		}
		return;
	    }
	}

	if (parsingMeta) {
	    doneWithMeta();
	}

	if (je.isDirectory()) {
	    mev.setEntry(null, je);
	    return;
	}

	// be liberal in what you accept. If the name starts with ./, remove
	// it as we internally canonicalize it with out the ./.
	if (name.startsWith("./"))
	    name = name.substring(2);

	// be liberal in what you accept. If the name starts with /, remove
	// it as we internally canonicalize it with out the /.
	if (name.startsWith("/"))
	    name = name.substring(1);

	// only set the jev object for entries that have a signature
	if (sigFileSigners.get(name) != null) {
	    mev.setEntry(name, je);
	    return;
	}

	// don't compute the digest for this entry
	mev.setEntry(null, je);

	return;
    }

    /**
     * update a single byte.
     */

    public void update(int b, ManifestEntryVerifier mev)
	throws IOException
    {
	if (b != -1) {
	    if (parsingBlockOrSF) {
		baos.write(b);
	    } else {
		mev.update((byte)b);
	    }
	} else {
	    processEntry(mev);
	}
    }

    /**
     * update an array of bytes.
     */

    public void update(int n, byte[] b, int off, int len,
		       ManifestEntryVerifier mev)
	throws IOException
    {
	if (n != -1) {
	    if (parsingBlockOrSF) {
		baos.write(b, off, n);
	    } else {
		mev.update(b, off, n);
	    }
	} else {
	    processEntry(mev);
	}
    }

    /**
     * called when we reach the end of entry in one of the read() methods.
     */
    private void processEntry(ManifestEntryVerifier mev)
	throws IOException
    {
	if (!parsingBlockOrSF) {
	    JarEntry je = mev.getEntry();
	    if ((je != null) && (je.signers == null)) {
		je.signers = mev.verify(verifiedSigners, sigFileSigners);
	    }
	} else {

	    try {
		parsingBlockOrSF = false;

		if (debug != null) {
		    debug.println("processEntry: processing block");
		}

		String uname = mev.getEntry().getName()
                                             .toUpperCase(Locale.ENGLISH);

		if (uname.endsWith(".SF")) {
		    String key = uname.substring(0, uname.length()-3);
		    byte bytes[] = baos.toByteArray();
		    // add to sigFileData in case future blocks need it
		    sigFileData.put(key, bytes);
		    // check pending blocks, we can now process
		    // anyone waiting for this .SF file
		    Iterator it = pendingBlocks.iterator();
		    while (it.hasNext()) {
			SignatureFileVerifier sfv =
			    (SignatureFileVerifier) it.next();
			if (sfv.needSignatureFile(key)) {
			    if (debug != null) {
				debug.println(
				 "processEntry: processing pending block");
			    }

			    sfv.setSignatureFile(bytes);
			    sfv.process(sigFileSigners);
			}
		    }
		    return;
		}

		// now we are parsing a signature block file

		String key = uname.substring(0, uname.lastIndexOf("."));

		if (signerCache == null)
		    signerCache = new ArrayList();

		if (manDig == null) {
		    synchronized(manifestRawBytes) {
			if (manDig == null) {
			    manDig = new ManifestDigester(manifestRawBytes);
			    manifestRawBytes = null;
			}
		    }
		}

		SignatureFileVerifier sfv =
		  new SignatureFileVerifier(signerCache,
					    manDig, uname, baos.toByteArray());

		if (sfv.needSignatureFileBytes()) {
		    // see if we have already parsed an external .SF file
		    byte[] bytes = (byte[]) sigFileData.get(key);

		    if (bytes == null) {
			// put this block on queue for later processing
			// since we don't have the .SF bytes yet
			// (uname, block);
			if (debug != null) {
			    debug.println("adding pending block");
			}
			pendingBlocks.add(sfv);
			return;
		    } else {
			sfv.setSignatureFile(bytes);
		    }
		}
		sfv.process(sigFileSigners);

	    } catch (sun.security.pkcs.ParsingException pe) {
		if (debug != null) debug.println("processEntry caught: "+pe);
		// ignore and treat as unsigned
	    } catch (IOException ioe) {
		if (debug != null) debug.println("processEntry caught: "+ioe);
		// ignore and treat as unsigned
	    } catch (SignatureException se) {
		if (debug != null) debug.println("processEntry caught: "+se);
		// ignore and treat as unsigned
	    } catch (NoSuchAlgorithmException nsae) {
		if (debug != null) debug.println("processEntry caught: "+nsae);
		// ignore and treat as unsigned
	    } catch (CertificateException ce) {
		if (debug != null) debug.println("processEntry caught: "+ce);
		// ignore and treat as unsigned
	    }
	}
    }

    /**
     * Return an array of java.security.cert.Certificate objects for
     * the given file in the jar. 
     */
    public java.security.cert.Certificate[] getCerts(String name)
    {
	CodeSigner[] signers = getCodeSigners(name);
	// Extract the certs in each code signer's cert chain
	if (signers != null) {
	    ArrayList certChains = new ArrayList();
	    for (int i = 0; i < signers.length; i++) {
		certChains.addAll(
		    signers[i].getSignerCertPath().getCertificates());
	    }

	    // Convert into a Certificate[]
	    return (java.security.cert.Certificate[])
		certChains.toArray(
		    new java.security.cert.Certificate[certChains.size()]);
	}
	return null;
    }

    /**
     * return an array of CodeSigner objects for
     * the given file in the jar. this array is not cloned.
     *
     */
    public CodeSigner[] getCodeSigners(String name)
    {
	return (CodeSigner[])verifiedSigners.get(name);
    }


    /**
     * returns true if there no files to verify.
     * should only be called after all the META-INF entries
     * have been processed.
     */
    boolean nothingToVerify()
    {
	return (anyToVerify == false);
    }

    /**
     * called to let us know we have processed all the
     * META-INF entries, and if we re-read one of them, don't
     * re-process it. Also gets rid of any data structures
     * we needed when parsing META-INF entries.
     */
    void doneWithMeta()
    {
	parsingMeta = false;
	anyToVerify = !sigFileSigners.isEmpty();
	baos = null;
	sigFileData = null;
	pendingBlocks = null;
	signerCache = null;
	manDig = null;
    }

    static class VerifierStream extends java.io.InputStream {

	private InputStream is;
	private JarVerifier jv;
	private ManifestEntryVerifier mev;
	private long numLeft;

	VerifierStream(Manifest man,
		       JarEntry je,
		       InputStream is,
		       JarVerifier jv) throws IOException
	{
	    this.is = is;
	    this.jv = jv;
	    this.mev = new ManifestEntryVerifier(man);
	    this.jv.beginEntry(je, mev);
	    this.numLeft = je.getSize();
	    if (this.numLeft == 0)
		this.jv.update(-1, this.mev);
	}

	public int read() throws IOException
	{
	    if (numLeft > 0) {
		int b = is.read();
		jv.update(b, mev);
		numLeft--;
		if (numLeft == 0)
		    jv.update(-1, mev);
		return b;
	    } else {
		return -1;
	    }
	}

	public int read(byte b[], int off, int len) throws IOException {
	    if ((numLeft > 0) && (numLeft < len)) {
		len = (int)numLeft;
	    }

	    if (numLeft > 0) {
		int n = is.read(b, off, len);
		jv.update(n, b, off, len, mev);
		numLeft -= n;
		if (numLeft == 0)
		    jv.update(-1, b, off, len, mev);
		return n;
	    } else {
		return -1;
	    }
	}

	public void close()
	    throws IOException
	{
	    if (is != null)
		is.close();
	    is = null;
	    mev = null;
	    jv = null;
	}

	public int available() throws IOException {
	    return is.available();
	}

    }
}
