001/* JarFile.java - Representation of a jar file
002   Copyright (C) 2000, 2003, 2004, 2005, 2006 Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package java.util.jar;
040
041import gnu.java.io.Base64InputStream;
042import gnu.java.security.OID;
043import gnu.java.security.pkcs.PKCS7SignedData;
044import gnu.java.security.pkcs.SignerInfo;
045import gnu.java.security.provider.Gnu;
046
047import java.io.ByteArrayOutputStream;
048import java.io.File;
049import java.io.FileNotFoundException;
050import java.io.FilterInputStream;
051import java.io.IOException;
052import java.io.InputStream;
053import java.security.InvalidKeyException;
054import java.security.MessageDigest;
055import java.security.NoSuchAlgorithmException;
056import java.security.Signature;
057import java.security.SignatureException;
058import java.security.cert.CRLException;
059import java.security.cert.Certificate;
060import java.security.cert.CertificateException;
061import java.security.cert.X509Certificate;
062import java.util.Arrays;
063import java.util.Enumeration;
064import java.util.HashMap;
065import java.util.HashSet;
066import java.util.Iterator;
067import java.util.LinkedList;
068import java.util.List;
069import java.util.Map;
070import java.util.Set;
071import java.util.regex.Matcher;
072import java.util.regex.Pattern;
073import java.util.zip.ZipEntry;
074import java.util.zip.ZipException;
075import java.util.zip.ZipFile;
076
077/**
078 * Representation of a jar file.
079 * <p>
080 * Note that this class is not a subclass of java.io.File but a subclass of
081 * java.util.zip.ZipFile and you can only read JarFiles with it (although
082 * there are constructors that take a File object).
083 *
084 * @since 1.2
085 * @author Mark Wielaard (mark@klomp.org)
086 * @author Casey Marshall (csm@gnu.org) wrote the certificate and entry
087 *  verification code.
088 */
089public class JarFile extends ZipFile
090{
091  // Fields
092
093  /** The name of the manifest entry: META-INF/MANIFEST.MF */
094  public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
095
096  /** The META-INF directory entry. */
097  private static final String META_INF = "META-INF/";
098
099  /** The suffix for PKCS7 DSA signature entries. */
100  private static final String PKCS7_DSA_SUFFIX = ".DSA";
101
102  /** The suffix for PKCS7 RSA signature entries. */
103  private static final String PKCS7_RSA_SUFFIX = ".RSA";
104
105  /** The suffix for digest attributes. */
106  private static final String DIGEST_KEY_SUFFIX = "-Digest";
107
108  /** The suffix for signature files. */
109  private static final String SF_SUFFIX = ".SF";
110
111  /**
112   * The security provider to use for signature verification.
113   * We need a known fallback to be able to read any signed jar file
114   * (which might contain the user selected security provider).
115   * This is package-private to avoid accessor methods for inner classes.
116   */
117  static final Gnu provider = new Gnu();
118
119  // Signature OIDs.
120  private static final OID MD2_OID = new OID("1.2.840.113549.2.2");
121  private static final OID MD4_OID = new OID("1.2.840.113549.2.4");
122  private static final OID MD5_OID = new OID("1.2.840.113549.2.5");
123  private static final OID SHA1_OID = new OID("1.3.14.3.2.26");
124  private static final OID DSA_ENCRYPTION_OID = new OID("1.2.840.10040.4.1");
125  private static final OID RSA_ENCRYPTION_OID = new OID("1.2.840.113549.1.1.1");
126
127  /**
128   * The manifest of this file, if any, otherwise null.
129   * Read when first needed.
130   */
131  private Manifest manifest;
132
133  /** Whether to verify the manifest and all entries. */
134  boolean verify;
135
136  /** Whether the has already been loaded. */
137  private boolean manifestRead = false;
138
139  /** Whether the signature files have been loaded. */
140  boolean signaturesRead = false;
141
142  /**
143   * A map between entry names and booleans, signaling whether or
144   * not that entry has been verified.
145   * Only be accessed with lock on this JarFile*/
146  HashMap verified = new HashMap();
147
148  /**
149   * A mapping from entry name to certificates, if any.
150   * Only accessed with lock on this JarFile.
151   */
152  HashMap entryCerts;
153
154  /**
155   * A {@link Map} of message digest algorithm names to their implementation.
156   * Used to reduce object (algorithm implementation) instantiation.
157   */
158  private HashMap digestAlgorithms = new HashMap();
159
160  static boolean DEBUG = false;
161  static void debug(Object msg)
162  {
163    System.err.print(JarFile.class.getName());
164    System.err.print(" >>> ");
165    System.err.println(msg);
166  }
167
168  // Constructors
169
170  /**
171   * Creates a new JarFile. All jar entries are verified (when a Manifest file
172   * for this JarFile exists). You need to actually open and read the complete
173   * jar entry (with <code>getInputStream()</code>) to check its signature.
174   *
175   * @param fileName the name of the file to open
176   * @exception FileNotFoundException if the fileName cannot be found
177   * @exception IOException if another IO exception occurs while reading
178   */
179  public JarFile(String fileName) throws FileNotFoundException, IOException
180  {
181    this(fileName, true);
182  }
183
184  /**
185   * Creates a new JarFile. If verify is true then all jar entries are
186   * verified (when a Manifest file for this JarFile exists). You need to
187   * actually open and read the complete jar entry
188   * (with <code>getInputStream()</code>) to check its signature.
189   *
190   * @param fileName the name of the file to open
191   * @param verify checks manifest and entries when true and a manifest
192   * exists, when false no checks are made
193   * @exception FileNotFoundException if the fileName cannot be found
194   * @exception IOException if another IO exception occurs while reading
195   */
196  public JarFile(String fileName, boolean verify) throws
197    FileNotFoundException, IOException
198  {
199    super(fileName);
200    if (verify)
201      {
202        manifest = readManifest();
203        verify();
204      }
205  }
206
207  /**
208   * Creates a new JarFile. All jar entries are verified (when a Manifest file
209   * for this JarFile exists). You need to actually open and read the complete
210   * jar entry (with <code>getInputStream()</code>) to check its signature.
211   *
212   * @param file the file to open as a jar file
213   * @exception FileNotFoundException if the file does not exits
214   * @exception IOException if another IO exception occurs while reading
215   */
216  public JarFile(File file) throws FileNotFoundException, IOException
217  {
218    this(file, true);
219  }
220
221  /**
222   * Creates a new JarFile. If verify is true then all jar entries are
223   * verified (when a Manifest file for this JarFile exists). You need to
224   * actually open and read the complete jar entry
225   * (with <code>getInputStream()</code>) to check its signature.
226   *
227   * @param file the file to open to open as a jar file
228   * @param verify checks manifest and entries when true and a manifest
229   * exists, when false no checks are made
230   * @exception FileNotFoundException if file does not exist
231   * @exception IOException if another IO exception occurs while reading
232   */
233  public JarFile(File file, boolean verify) throws FileNotFoundException,
234    IOException
235  {
236    super(file);
237    if (verify)
238      {
239        manifest = readManifest();
240        verify();
241      }
242  }
243
244  /**
245   * Creates a new JarFile with the indicated mode. If verify is true then
246   * all jar entries are verified (when a Manifest file for this JarFile
247   * exists). You need to actually open and read the complete jar entry
248   * (with <code>getInputStream()</code>) to check its signature.
249   * manifest and if the manifest exists and verify is true verfies it.
250   *
251   * @param file the file to open to open as a jar file
252   * @param verify checks manifest and entries when true and a manifest
253   * exists, when false no checks are made
254   * @param mode either ZipFile.OPEN_READ or
255   *             (ZipFile.OPEN_READ | ZipFile.OPEN_DELETE)
256   * @exception FileNotFoundException if the file does not exist
257   * @exception IOException if another IO exception occurs while reading
258   * @exception IllegalArgumentException when given an illegal mode
259   * 
260   * @since 1.3
261   */
262  public JarFile(File file, boolean verify, int mode) throws
263    FileNotFoundException, IOException, IllegalArgumentException
264  {
265    super(file, mode);
266    if (verify)
267      {
268        manifest = readManifest();
269        verify();
270      }
271  }
272
273  // Methods
274
275  /**
276   * XXX - should verify the manifest file
277   */
278  private void verify()
279  {
280    // only check if manifest is not null
281    if (manifest == null)
282      {
283        verify = false;
284        return;
285      }
286
287    verify = true;
288    // XXX - verify manifest
289  }
290
291  /**
292   * Parses and returns the manifest if it exists, otherwise returns null.
293   */
294  private Manifest readManifest()
295  {
296    try
297      {
298        ZipEntry manEntry = super.getEntry(MANIFEST_NAME);
299        if (manEntry != null)
300          {
301            InputStream in = super.getInputStream(manEntry);
302            manifestRead = true;
303            return new Manifest(in);
304          }
305        else
306          {
307            manifestRead = true;
308            return null;
309          }
310      }
311    catch (IOException ioe)
312      {
313        manifestRead = true;
314        return null;
315      }
316  }
317
318  /**
319   * Returns a enumeration of all the entries in the JarFile.
320   * Note that also the Jar META-INF entries are returned.
321   *
322   * @exception IllegalStateException when the JarFile is already closed
323   */
324  public Enumeration<JarEntry> entries() throws IllegalStateException
325  {
326    return new JarEnumeration(super.entries(), this);
327  }
328
329  /**
330   * Wraps a given Zip Entries Enumeration. For every zip entry a
331   * JarEntry is created and the corresponding Attributes are looked up.
332   */
333  private static class JarEnumeration implements Enumeration<JarEntry>
334  {
335
336    private final Enumeration<? extends ZipEntry> entries;
337    private final JarFile jarfile;
338
339    JarEnumeration(Enumeration<? extends ZipEntry> e, JarFile f)
340    {
341      entries = e;
342      jarfile = f;
343    }
344
345    public boolean hasMoreElements()
346    {
347      return entries.hasMoreElements();
348    }
349
350    public JarEntry nextElement()
351    {
352      ZipEntry zip = (ZipEntry) entries.nextElement();
353      JarEntry jar = new JarEntry(zip);
354      Manifest manifest;
355      try
356        {
357          manifest = jarfile.getManifest();
358        }
359      catch (IOException ioe)
360        {
361          manifest = null;
362        }
363
364      if (manifest != null)
365        {
366          jar.attr = manifest.getAttributes(jar.getName());
367        }
368
369      synchronized(jarfile)
370        {
371          if (jarfile.verify && !jarfile.signaturesRead)
372            try
373              {
374                jarfile.readSignatures();
375              }
376            catch (IOException ioe)
377              {
378                if (JarFile.DEBUG)
379                  {
380                    JarFile.debug(ioe);
381                    ioe.printStackTrace();
382                  }
383                jarfile.signaturesRead = true; // fudge it.
384              }
385        }
386      jar.jarfile = jarfile;
387      return jar;
388    }
389  }
390
391  /**
392   * XXX
393   * It actually returns a JarEntry not a zipEntry
394   * @param name XXX
395   */
396  public synchronized ZipEntry getEntry(String name)
397  {
398    ZipEntry entry = super.getEntry(name);
399    if (entry != null)
400      {
401        JarEntry jarEntry = new JarEntry(entry);
402        Manifest manifest;
403        try
404          {
405            manifest = getManifest();
406          }
407        catch (IOException ioe)
408          {
409            manifest = null;
410          }
411
412        if (manifest != null)
413          {
414            jarEntry.attr = manifest.getAttributes(name);
415          }
416
417        if (verify && !signaturesRead)
418          try
419            {
420              readSignatures();
421            }
422          catch (IOException ioe)
423            {
424              if (DEBUG)
425                {
426                  debug(ioe);
427                  ioe.printStackTrace();
428                }
429              signaturesRead = true;
430            }
431        jarEntry.jarfile = this;
432        return jarEntry;
433      }
434    return null;
435  }
436
437  /**
438   * Returns an input stream for the given entry. If configured to
439   * verify entries, the input stream returned will verify them while
440   * the stream is read, but only on the first time.
441   *
442   * @param entry The entry to get the input stream for.
443   * @exception ZipException XXX
444   * @exception IOException XXX
445   */
446  public synchronized InputStream getInputStream(ZipEntry entry) throws
447    ZipException, IOException
448  {
449    // If we haven't verified the hash, do it now.
450    if (!verified.containsKey(entry.getName()) && verify)
451      {
452        if (DEBUG)
453          debug("reading and verifying " + entry);
454        return new EntryInputStream(entry, super.getInputStream(entry), this);
455      }
456    else
457      {
458        if (DEBUG)
459          debug("reading already verified entry " + entry);
460        if (verify && verified.get(entry.getName()) == Boolean.FALSE)
461          throw new ZipException("digest for " + entry + " is invalid");
462        return super.getInputStream(entry);
463      }
464  }
465
466  /**
467   * Returns the JarEntry that belongs to the name if such an entry
468   * exists in the JarFile. Returns null otherwise
469   * Convenience method that just casts the result from <code>getEntry</code>
470   * to a JarEntry.
471   *
472   * @param name the jar entry name to look up
473   * @return the JarEntry if it exists, null otherwise
474   */
475  public JarEntry getJarEntry(String name)
476  {
477    return (JarEntry) getEntry(name);
478  }
479
480  /**
481   * Returns the manifest for this JarFile or null when the JarFile does not
482   * contain a manifest file.
483   */
484  public synchronized Manifest getManifest() throws IOException
485  {
486    if (!manifestRead)
487      manifest = readManifest();
488
489    return manifest;
490  }
491
492  // Only called with lock on this JarFile.
493  // Package private for use in inner classes.
494  void readSignatures() throws IOException
495  {
496    Map pkcs7Dsa = new HashMap();
497    Map pkcs7Rsa = new HashMap();
498    Map sigFiles = new HashMap();
499
500    // Phase 1: Read all signature files. These contain the user
501    // certificates as well as the signatures themselves.
502    for (Enumeration e = super.entries(); e.hasMoreElements(); )
503      {
504        ZipEntry ze = (ZipEntry) e.nextElement();
505        String name = ze.getName();
506        if (name.startsWith(META_INF))
507          {
508            String alias = name.substring(META_INF.length());
509            if (alias.lastIndexOf('.') >= 0)
510              alias = alias.substring(0, alias.lastIndexOf('.'));
511
512            if (name.endsWith(PKCS7_DSA_SUFFIX) || name.endsWith(PKCS7_RSA_SUFFIX))
513              {
514                if (DEBUG)
515                  debug("reading PKCS7 info from " + name + ", alias=" + alias);
516                PKCS7SignedData sig = null;
517                try
518                  {
519                    sig = new PKCS7SignedData(super.getInputStream(ze));
520                  }
521                catch (CertificateException ce)
522                  {
523                    IOException ioe = new IOException("certificate parsing error");
524                    ioe.initCause(ce);
525                    throw ioe;
526                  }
527                catch (CRLException crle)
528                  {
529                    IOException ioe = new IOException("CRL parsing error");
530                    ioe.initCause(crle);
531                    throw ioe;
532                  }
533                if (name.endsWith(PKCS7_DSA_SUFFIX))
534                  pkcs7Dsa.put(alias, sig);
535                else if (name.endsWith(PKCS7_RSA_SUFFIX))
536                  pkcs7Rsa.put(alias, sig);
537              }
538            else if (name.endsWith(SF_SUFFIX))
539              {
540                if (DEBUG)
541                  debug("reading signature file for " + alias + ": " + name);
542                Manifest sf = new Manifest(super.getInputStream(ze));
543                sigFiles.put(alias, sf);
544                if (DEBUG)
545                  debug("result: " + sf);
546              }
547          }
548      }
549
550    // Phase 2: verify the signatures on any signature files.
551    Set validCerts = new HashSet();
552    Map entryCerts = new HashMap();
553    for (Iterator it = sigFiles.entrySet().iterator(); it.hasNext(); )
554      {
555        int valid = 0;
556        Map.Entry e = (Map.Entry) it.next();
557        String alias = (String) e.getKey();
558
559        PKCS7SignedData sig = (PKCS7SignedData) pkcs7Dsa.get(alias);
560        if (sig != null)
561          {
562            Certificate[] certs = sig.getCertificates();
563            Set signerInfos = sig.getSignerInfos();
564            for (Iterator it2 = signerInfos.iterator(); it2.hasNext(); )
565              verify(certs, (SignerInfo) it2.next(), alias, validCerts);
566          }
567
568        sig = (PKCS7SignedData) pkcs7Rsa.get(alias);
569        if (sig != null)
570          {
571            Certificate[] certs = sig.getCertificates();
572            Set signerInfos = sig.getSignerInfos();
573            for (Iterator it2 = signerInfos.iterator(); it2.hasNext(); )
574              verify(certs, (SignerInfo) it2.next(), alias, validCerts);
575          }
576
577        // It isn't a signature for anything. Punt it.
578        if (validCerts.isEmpty())
579          {
580            it.remove();
581            continue;
582          }
583
584        entryCerts.put(e.getValue(), new HashSet(validCerts));
585        validCerts.clear();
586      }
587
588    // Read the manifest into a HashMap (String fileName, String entry)
589    // The fileName might be split into multiple lines in the manifest.
590    // Such additional lines will start with a space.
591    InputStream in = super.getInputStream(super.getEntry(MANIFEST_NAME));
592    ByteArrayOutputStream baStream = new ByteArrayOutputStream();
593    byte[] ba = new byte[1024];
594    while (true)
595      {
596        int len = in.read(ba);
597        if (len < 0)
598          break;
599        baStream.write(ba, 0, len);
600      }
601    in.close();
602
603    HashMap hmManifestEntries = new HashMap();
604    Pattern p = Pattern.compile("Name: (.+?\r?\n(?: .+?\r?\n)*)"
605                                + ".+?-Digest: .+?\r?\n\r?\n");
606    Matcher m = p.matcher(baStream.toString());
607    while (m.find())
608      {
609        String fileName = m.group(1).replaceAll("\r?\n ?", "");
610        hmManifestEntries.put(fileName, m.group());
611      }
612
613    // Phase 3: verify the signature file signatures against the manifest,
614    // mapping the entry name to the target certificates.
615    this.entryCerts = new HashMap();
616    for (Iterator it = entryCerts.entrySet().iterator(); it.hasNext(); )
617      {
618        Map.Entry e = (Map.Entry) it.next();
619        Manifest sigfile = (Manifest) e.getKey();
620        Map entries = sigfile.getEntries();
621        Set certificates = (Set) e.getValue();
622
623        for (Iterator it2 = entries.entrySet().iterator(); it2.hasNext(); )
624          {
625            Map.Entry e2 = (Map.Entry) it2.next();
626            String entryname = String.valueOf(e2.getKey());
627            Attributes attr = (Attributes) e2.getValue();
628            if (verifyHashes(entryname, attr, hmManifestEntries))
629              {
630                if (DEBUG)
631                  debug("entry " + entryname + " has certificates " + certificates);
632                Set s = (Set) this.entryCerts.get(entryname);
633                if (s != null)
634                  s.addAll(certificates);
635                else
636                  this.entryCerts.put(entryname, new HashSet(certificates));
637              }
638          }
639      }
640
641    signaturesRead = true;
642  }
643
644  /**
645   * Tell if the given signer info is over the given alias's signature file,
646   * given one of the certificates specified.
647   */
648  private void verify(Certificate[] certs, SignerInfo signerInfo,
649                      String alias, Set validCerts)
650  {
651    Signature sig = null;
652    try
653      {
654        OID alg = signerInfo.getDigestEncryptionAlgorithmId();
655        if (alg.equals(DSA_ENCRYPTION_OID))
656          {
657            if (!signerInfo.getDigestAlgorithmId().equals(SHA1_OID))
658              return;
659            sig = Signature.getInstance("SHA1withDSA", provider);
660          }
661        else if (alg.equals(RSA_ENCRYPTION_OID))
662          {
663            OID hash = signerInfo.getDigestAlgorithmId();
664            if (hash.equals(MD2_OID))
665              sig = Signature.getInstance("md2WithRsaEncryption", provider);
666            else if (hash.equals(MD4_OID))
667              sig = Signature.getInstance("md4WithRsaEncryption", provider);
668            else if (hash.equals(MD5_OID))
669              sig = Signature.getInstance("md5WithRsaEncryption", provider);
670            else if (hash.equals(SHA1_OID))
671              sig = Signature.getInstance("sha1WithRsaEncryption", provider);
672            else
673              return;
674          }
675        else
676          {
677            if (DEBUG)
678              debug("unsupported signature algorithm: " + alg);
679            return;
680          }
681      }
682    catch (NoSuchAlgorithmException nsae)
683      {
684        if (DEBUG)
685          {
686            debug(nsae);
687            nsae.printStackTrace();
688          }
689        return;
690      }
691    ZipEntry sigFileEntry = super.getEntry(META_INF + alias + SF_SUFFIX);
692    if (sigFileEntry == null)
693      return;
694    for (int i = 0; i < certs.length; i++)
695      {
696        if (!(certs[i] instanceof X509Certificate))
697          continue;
698        X509Certificate cert = (X509Certificate) certs[i];
699        if (!cert.getIssuerX500Principal().equals(signerInfo.getIssuer()) ||
700            !cert.getSerialNumber().equals(signerInfo.getSerialNumber()))
701          continue;
702        try
703          {
704            sig.initVerify(cert.getPublicKey());
705            InputStream in = super.getInputStream(sigFileEntry);
706            if (in == null)
707              continue;
708            byte[] buf = new byte[1024];
709            int len = 0;
710            while ((len = in.read(buf)) != -1)
711              sig.update(buf, 0, len);
712            if (sig.verify(signerInfo.getEncryptedDigest()))
713              {
714                if (DEBUG)
715                  debug("signature for " + cert.getSubjectDN() + " is good");
716                validCerts.add(cert);
717              }
718          }
719        catch (IOException ioe)
720          {
721            continue;
722          }
723        catch (InvalidKeyException ike)
724          {
725            continue;
726          }
727        catch (SignatureException se)
728          {
729            continue;
730          }
731      }
732  }
733
734  /**
735   * Verifies that the digest(s) in a signature file were, in fact, made over
736   * the manifest entry for ENTRY.
737   * 
738   * @param entry The entry name.
739   * @param attr The attributes from the signature file to verify.
740   * @param hmManifestEntries Mappings of Jar file entry names to their manifest
741   *          entry text; i.e. the base-64 encoding of their 
742   */
743  private boolean verifyHashes(String entry, Attributes attr,
744                               HashMap hmManifestEntries)
745  {
746    int verified = 0;
747
748    String stringEntry = (String) hmManifestEntries.get(entry);
749    if (stringEntry == null)
750      {
751        if (DEBUG)
752          debug("could not find " + entry + " in manifest");
753        return false;
754      }
755    // The bytes for ENTRY's manifest entry, which are signed in the
756    // signature file.
757    byte[] entryBytes = stringEntry.getBytes();
758
759    for (Iterator it = attr.entrySet().iterator(); it.hasNext(); )
760      {
761        Map.Entry e = (Map.Entry) it.next();
762        String key = String.valueOf(e.getKey());
763        if (!key.endsWith(DIGEST_KEY_SUFFIX))
764          continue;
765        String alg = key.substring(0, key.length() - DIGEST_KEY_SUFFIX.length());
766        try
767          {
768            byte[] hash = Base64InputStream.decode((String) e.getValue());
769            MessageDigest md = (MessageDigest) digestAlgorithms.get(alg);
770            if (md == null)
771              {
772                md = MessageDigest.getInstance(alg, provider);
773                digestAlgorithms.put(alg, md);
774              }
775            md.reset();
776            byte[] hash2 = md.digest(entryBytes);
777            if (DEBUG)
778              debug("verifying SF entry " + entry + " alg: " + md.getAlgorithm()
779                    + " expect=" + new java.math.BigInteger(hash).toString(16)
780                    + " comp=" + new java.math.BigInteger(hash2).toString(16));
781            if (!Arrays.equals(hash, hash2))
782              return false;
783            verified++;
784          }
785        catch (IOException ioe)
786          {
787            if (DEBUG)
788              {
789                debug(ioe);
790                ioe.printStackTrace();
791              }
792            return false;
793          }
794        catch (NoSuchAlgorithmException nsae)
795          {
796            if (DEBUG)
797              {
798                debug(nsae);
799                nsae.printStackTrace();
800              }
801            return false;
802          }
803      }
804
805    // We have to find at least one valid digest.
806    return verified > 0;
807  }
808
809  /**
810   * A utility class that verifies jar entries as they are read.
811   */
812  private static class EntryInputStream extends FilterInputStream
813  {
814    private final JarFile jarfile;
815    private final long length;
816    private long pos;
817    private final ZipEntry entry;
818    private final byte[][] hashes;
819    private final MessageDigest[] md;
820    private boolean checked;
821
822    EntryInputStream(final ZipEntry entry,
823                     final InputStream in,
824                     final JarFile jar)
825      throws IOException
826    {
827      super(in);
828      this.entry = entry;
829      this.jarfile = jar;
830
831      length = entry.getSize();
832      pos = 0;
833      checked = false;
834
835      Attributes attr;
836      Manifest manifest = jarfile.getManifest();
837      if (manifest != null)
838        attr = manifest.getAttributes(entry.getName());
839      else
840        attr = null;
841      if (DEBUG)
842        debug("verifying entry " + entry + " attr=" + attr);
843      if (attr == null)
844        {
845          hashes = new byte[0][];
846          md = new MessageDigest[0];
847        }
848      else
849        {
850          List hashes = new LinkedList();
851          List md = new LinkedList();
852          for (Iterator it = attr.entrySet().iterator(); it.hasNext(); )
853            {
854              Map.Entry e = (Map.Entry) it.next();
855              String key = String.valueOf(e.getKey());
856              if (key == null)
857                continue;
858              if (!key.endsWith(DIGEST_KEY_SUFFIX))
859                continue;
860              hashes.add(Base64InputStream.decode((String) e.getValue()));
861              try
862                {
863                  int length = key.length() - DIGEST_KEY_SUFFIX.length();
864                  String alg = key.substring(0, length);
865                  md.add(MessageDigest.getInstance(alg, provider));
866                }
867              catch (NoSuchAlgorithmException nsae)
868                {
869                  IOException ioe = new IOException("no such message digest: " + key);
870                  ioe.initCause(nsae);
871                  throw ioe;
872                }
873            }
874          if (DEBUG)
875            debug("digests=" + md);
876          this.hashes = (byte[][]) hashes.toArray(new byte[hashes.size()][]);
877          this.md = (MessageDigest[]) md.toArray(new MessageDigest[md.size()]);
878        }
879    }
880
881    public boolean markSupported()
882    {
883      return false;
884    }
885
886    public void mark(int readLimit)
887    {
888    }
889
890    public void reset()
891    {
892    }
893
894    public int read() throws IOException
895    {
896      int b = super.read();
897      if (b == -1)
898        {
899          eof();
900          return -1;
901        }
902      for (int i = 0; i < md.length; i++)
903        md[i].update((byte) b);
904      pos++;
905      if (length > 0 && pos >= length)
906        eof();
907      return b;
908    }
909
910    public int read(byte[] buf, int off, int len) throws IOException
911    {
912      int count = super.read(buf, off, (int) Math.min(len, (length != 0
913                                                            ? length - pos
914                                                            : Integer.MAX_VALUE)));
915      if (count == -1 || (length > 0 && pos >= length))
916        {
917          eof();
918          return -1;
919        }
920      for (int i = 0; i < md.length; i++)
921        md[i].update(buf, off, count);
922      pos += count;
923      if (length != 0 && pos >= length)
924        eof();
925      return count;
926    }
927
928    public int read(byte[] buf) throws IOException
929    {
930      return read(buf, 0, buf.length);
931    }
932
933    public long skip(long bytes) throws IOException
934    {
935      byte[] b = new byte[1024];
936      long amount = 0;
937      while (amount < bytes)
938        {
939          int l = read(b, 0, (int) Math.min(b.length, bytes - amount));
940          if (l == -1)
941            break;
942          amount += l;
943        }
944      return amount;
945    }
946
947    private void eof() throws IOException
948    {
949      if (checked)
950        return;
951      checked = true;
952      for (int i = 0; i < md.length; i++)
953        {
954          byte[] hash = md[i].digest();
955          if (DEBUG)
956            debug("verifying " + md[i].getAlgorithm() + " expect="
957                  + new java.math.BigInteger(hashes[i]).toString(16)
958                  + " comp=" + new java.math.BigInteger(hash).toString(16));
959          if (!Arrays.equals(hash, hashes[i]))
960            {
961              synchronized(jarfile)
962                {
963                  if (DEBUG)
964                    debug(entry + " could NOT be verified");
965                  jarfile.verified.put(entry.getName(), Boolean.FALSE);
966                }
967              return;
968              // XXX ??? what do we do here?
969              // throw new ZipException("message digest mismatch");
970            }
971        }
972
973      synchronized(jarfile)
974        {
975          if (DEBUG)
976            debug(entry + " has been VERIFIED");
977          jarfile.verified.put(entry.getName(), Boolean.TRUE);
978        }
979    }
980  }
981}