| /* JarFile.java - Representation of a jar file |
| Copyright (C) 2000, 2003, 2004, 2005, 2006 Free Software Foundation, Inc. |
| |
| This file is part of GNU Classpath. |
| |
| GNU Classpath is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 2, or (at your option) |
| any later version. |
| |
| GNU Classpath 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 for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with GNU Classpath; see the file COPYING. If not, write to the |
| Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| 02110-1301 USA. |
| |
| Linking this library statically or dynamically with other modules is |
| making a combined work based on this library. Thus, the terms and |
| conditions of the GNU General Public License cover the whole |
| combination. |
| |
| As a special exception, the copyright holders of this library give you |
| permission to link this library with independent modules to produce an |
| executable, regardless of the license terms of these independent |
| modules, and to copy and distribute the resulting executable under |
| terms of your choice, provided that you also meet, for each linked |
| independent module, the terms and conditions of the license of that |
| module. An independent module is a module which is not derived from |
| or based on this library. If you modify this library, you may extend |
| this exception to your version of the library, but you are not |
| obligated to do so. If you do not wish to do so, delete this |
| exception statement from your version. */ |
| |
| |
| package java.util.jar; |
| |
| import gnu.java.io.Base64InputStream; |
| import gnu.java.security.OID; |
| import gnu.java.security.pkcs.PKCS7SignedData; |
| import gnu.java.security.pkcs.SignerInfo; |
| import gnu.java.security.provider.Gnu; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FilterInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.security.InvalidKeyException; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.Signature; |
| import java.security.SignatureException; |
| import java.security.cert.CRLException; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateException; |
| import java.security.cert.X509Certificate; |
| import java.util.Arrays; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipException; |
| import java.util.zip.ZipFile; |
| |
| /** |
| * Representation of a jar file. |
| * <p> |
| * Note that this class is not a subclass of java.io.File but a subclass of |
| * java.util.zip.ZipFile and you can only read JarFiles with it (although |
| * there are constructors that take a File object). |
| * |
| * @since 1.2 |
| * @author Mark Wielaard (mark@klomp.org) |
| * @author Casey Marshall (csm@gnu.org) wrote the certificate and entry |
| * verification code. |
| */ |
| public class JarFile extends ZipFile |
| { |
| // Fields |
| |
| /** The name of the manifest entry: META-INF/MANIFEST.MF */ |
| public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; |
| |
| /** The META-INF directory entry. */ |
| private static final String META_INF = "META-INF/"; |
| |
| /** The suffix for PKCS7 DSA signature entries. */ |
| private static final String PKCS7_DSA_SUFFIX = ".DSA"; |
| |
| /** The suffix for PKCS7 RSA signature entries. */ |
| private static final String PKCS7_RSA_SUFFIX = ".RSA"; |
| |
| /** The suffix for digest attributes. */ |
| private static final String DIGEST_KEY_SUFFIX = "-Digest"; |
| |
| /** The suffix for signature files. */ |
| private static final String SF_SUFFIX = ".SF"; |
| |
| /** |
| * The security provider to use for signature verification. |
| * We need a known fallback to be able to read any signed jar file |
| * (which might contain the user selected security provider). |
| * This is package-private to avoid accessor methods for inner classes. |
| */ |
| static final Gnu provider = new Gnu(); |
| |
| // Signature OIDs. |
| private static final OID MD2_OID = new OID("1.2.840.113549.2.2"); |
| private static final OID MD4_OID = new OID("1.2.840.113549.2.4"); |
| private static final OID MD5_OID = new OID("1.2.840.113549.2.5"); |
| private static final OID SHA1_OID = new OID("1.3.14.3.2.26"); |
| private static final OID DSA_ENCRYPTION_OID = new OID("1.2.840.10040.4.1"); |
| private static final OID RSA_ENCRYPTION_OID = new OID("1.2.840.113549.1.1.1"); |
| |
| /** |
| * The manifest of this file, if any, otherwise null. |
| * Read when first needed. |
| */ |
| private Manifest manifest; |
| |
| /** Whether to verify the manifest and all entries. */ |
| boolean verify; |
| |
| /** Whether the has already been loaded. */ |
| private boolean manifestRead = false; |
| |
| /** Whether the signature files have been loaded. */ |
| boolean signaturesRead = false; |
| |
| /** |
| * A map between entry names and booleans, signaling whether or |
| * not that entry has been verified. |
| * Only be accessed with lock on this JarFile*/ |
| HashMap verified = new HashMap(); |
| |
| /** |
| * A mapping from entry name to certificates, if any. |
| * Only accessed with lock on this JarFile. |
| */ |
| HashMap entryCerts; |
| |
| static boolean DEBUG = false; |
| static void debug(Object msg) |
| { |
| System.err.print(JarFile.class.getName()); |
| System.err.print(" >>> "); |
| System.err.println(msg); |
| } |
| |
| // Constructors |
| |
| /** |
| * Creates a new JarFile. All jar entries are verified (when a Manifest file |
| * for this JarFile exists). You need to actually open and read the complete |
| * jar entry (with <code>getInputStream()</code>) to check its signature. |
| * |
| * @param fileName the name of the file to open |
| * @exception FileNotFoundException if the fileName cannot be found |
| * @exception IOException if another IO exception occurs while reading |
| */ |
| public JarFile(String fileName) throws FileNotFoundException, IOException |
| { |
| this(fileName, true); |
| } |
| |
| /** |
| * Creates a new JarFile. If verify is true then all jar entries are |
| * verified (when a Manifest file for this JarFile exists). You need to |
| * actually open and read the complete jar entry |
| * (with <code>getInputStream()</code>) to check its signature. |
| * |
| * @param fileName the name of the file to open |
| * @param verify checks manifest and entries when true and a manifest |
| * exists, when false no checks are made |
| * @exception FileNotFoundException if the fileName cannot be found |
| * @exception IOException if another IO exception occurs while reading |
| */ |
| public JarFile(String fileName, boolean verify) throws |
| FileNotFoundException, IOException |
| { |
| super(fileName); |
| if (verify) |
| { |
| manifest = readManifest(); |
| verify(); |
| } |
| } |
| |
| /** |
| * Creates a new JarFile. All jar entries are verified (when a Manifest file |
| * for this JarFile exists). You need to actually open and read the complete |
| * jar entry (with <code>getInputStream()</code>) to check its signature. |
| * |
| * @param file the file to open as a jar file |
| * @exception FileNotFoundException if the file does not exits |
| * @exception IOException if another IO exception occurs while reading |
| */ |
| public JarFile(File file) throws FileNotFoundException, IOException |
| { |
| this(file, true); |
| } |
| |
| /** |
| * Creates a new JarFile. If verify is true then all jar entries are |
| * verified (when a Manifest file for this JarFile exists). You need to |
| * actually open and read the complete jar entry |
| * (with <code>getInputStream()</code>) to check its signature. |
| * |
| * @param file the file to open to open as a jar file |
| * @param verify checks manifest and entries when true and a manifest |
| * exists, when false no checks are made |
| * @exception FileNotFoundException if file does not exist |
| * @exception IOException if another IO exception occurs while reading |
| */ |
| public JarFile(File file, boolean verify) throws FileNotFoundException, |
| IOException |
| { |
| super(file); |
| if (verify) |
| { |
| manifest = readManifest(); |
| verify(); |
| } |
| } |
| |
| /** |
| * Creates a new JarFile with the indicated mode. If verify is true then |
| * all jar entries are verified (when a Manifest file for this JarFile |
| * exists). You need to actually open and read the complete jar entry |
| * (with <code>getInputStream()</code>) to check its signature. |
| * manifest and if the manifest exists and verify is true verfies it. |
| * |
| * @param file the file to open to open as a jar file |
| * @param verify checks manifest and entries when true and a manifest |
| * exists, when false no checks are made |
| * @param mode either ZipFile.OPEN_READ or |
| * (ZipFile.OPEN_READ | ZipFile.OPEN_DELETE) |
| * @exception FileNotFoundException if the file does not exist |
| * @exception IOException if another IO exception occurs while reading |
| * @exception IllegalArgumentException when given an illegal mode |
| * |
| * @since 1.3 |
| */ |
| public JarFile(File file, boolean verify, int mode) throws |
| FileNotFoundException, IOException, IllegalArgumentException |
| { |
| super(file, mode); |
| if (verify) |
| { |
| manifest = readManifest(); |
| verify(); |
| } |
| } |
| |
| // Methods |
| |
| /** |
| * XXX - should verify the manifest file |
| */ |
| private void verify() |
| { |
| // only check if manifest is not null |
| if (manifest == null) |
| { |
| verify = false; |
| return; |
| } |
| |
| verify = true; |
| // XXX - verify manifest |
| } |
| |
| /** |
| * Parses and returns the manifest if it exists, otherwise returns null. |
| */ |
| private Manifest readManifest() |
| { |
| try |
| { |
| ZipEntry manEntry = super.getEntry(MANIFEST_NAME); |
| if (manEntry != null) |
| { |
| InputStream in = super.getInputStream(manEntry); |
| manifestRead = true; |
| return new Manifest(in); |
| } |
| else |
| { |
| manifestRead = true; |
| return null; |
| } |
| } |
| catch (IOException ioe) |
| { |
| manifestRead = true; |
| return null; |
| } |
| } |
| |
| /** |
| * Returns a enumeration of all the entries in the JarFile. |
| * Note that also the Jar META-INF entries are returned. |
| * |
| * @exception IllegalStateException when the JarFile is already closed |
| */ |
| public Enumeration entries() throws IllegalStateException |
| { |
| return new JarEnumeration(super.entries(), this); |
| } |
| |
| /** |
| * Wraps a given Zip Entries Enumeration. For every zip entry a |
| * JarEntry is created and the corresponding Attributes are looked up. |
| */ |
| private static class JarEnumeration implements Enumeration |
| { |
| |
| private final Enumeration entries; |
| private final JarFile jarfile; |
| |
| JarEnumeration(Enumeration e, JarFile f) |
| { |
| entries = e; |
| jarfile = f; |
| } |
| |
| public boolean hasMoreElements() |
| { |
| return entries.hasMoreElements(); |
| } |
| |
| public Object nextElement() |
| { |
| ZipEntry zip = (ZipEntry) entries.nextElement(); |
| JarEntry jar = new JarEntry(zip); |
| Manifest manifest; |
| try |
| { |
| manifest = jarfile.getManifest(); |
| } |
| catch (IOException ioe) |
| { |
| manifest = null; |
| } |
| |
| if (manifest != null) |
| { |
| jar.attr = manifest.getAttributes(jar.getName()); |
| } |
| |
| synchronized(jarfile) |
| { |
| if (jarfile.verify && !jarfile.signaturesRead) |
| try |
| { |
| jarfile.readSignatures(); |
| } |
| catch (IOException ioe) |
| { |
| if (JarFile.DEBUG) |
| { |
| JarFile.debug(ioe); |
| ioe.printStackTrace(); |
| } |
| jarfile.signaturesRead = true; // fudge it. |
| } |
| |
| // Include the certificates only if we have asserted that the |
| // signatures are valid. This means the certificates will not be |
| // available if the entry hasn't been read yet. |
| if (jarfile.entryCerts != null |
| && jarfile.verified.get(zip.getName()) == Boolean.TRUE) |
| { |
| Set certs = (Set) jarfile.entryCerts.get(jar.getName()); |
| if (certs != null) |
| jar.certs = (Certificate[]) |
| certs.toArray(new Certificate[certs.size()]); |
| } |
| } |
| return jar; |
| } |
| } |
| |
| /** |
| * XXX |
| * It actually returns a JarEntry not a zipEntry |
| * @param name XXX |
| */ |
| public synchronized ZipEntry getEntry(String name) |
| { |
| ZipEntry entry = super.getEntry(name); |
| if (entry != null) |
| { |
| JarEntry jarEntry = new JarEntry(entry); |
| Manifest manifest; |
| try |
| { |
| manifest = getManifest(); |
| } |
| catch (IOException ioe) |
| { |
| manifest = null; |
| } |
| |
| if (manifest != null) |
| { |
| jarEntry.attr = manifest.getAttributes(name); |
| } |
| |
| if (verify && !signaturesRead) |
| try |
| { |
| readSignatures(); |
| } |
| catch (IOException ioe) |
| { |
| if (DEBUG) |
| { |
| debug(ioe); |
| ioe.printStackTrace(); |
| } |
| signaturesRead = true; |
| } |
| // See the comments in the JarEnumeration for why we do this |
| // check. |
| if (DEBUG) |
| debug("entryCerts=" + entryCerts + " verified " + name |
| + " ? " + verified.get(name)); |
| if (entryCerts != null && verified.get(name) == Boolean.TRUE) |
| { |
| Set certs = (Set) entryCerts.get(name); |
| if (certs != null) |
| jarEntry.certs = (Certificate[]) |
| certs.toArray(new Certificate[certs.size()]); |
| } |
| return jarEntry; |
| } |
| return null; |
| } |
| |
| /** |
| * Returns an input stream for the given entry. If configured to |
| * verify entries, the input stream returned will verify them while |
| * the stream is read, but only on the first time. |
| * |
| * @param entry The entry to get the input stream for. |
| * @exception ZipException XXX |
| * @exception IOException XXX |
| */ |
| public synchronized InputStream getInputStream(ZipEntry entry) throws |
| ZipException, IOException |
| { |
| // If we haven't verified the hash, do it now. |
| if (!verified.containsKey(entry.getName()) && verify) |
| { |
| if (DEBUG) |
| debug("reading and verifying " + entry); |
| return new EntryInputStream(entry, super.getInputStream(entry), this); |
| } |
| else |
| { |
| if (DEBUG) |
| debug("reading already verified entry " + entry); |
| if (verify && verified.get(entry.getName()) == Boolean.FALSE) |
| throw new ZipException("digest for " + entry + " is invalid"); |
| return super.getInputStream(entry); |
| } |
| } |
| |
| /** |
| * Returns the JarEntry that belongs to the name if such an entry |
| * exists in the JarFile. Returns null otherwise |
| * Convenience method that just casts the result from <code>getEntry</code> |
| * to a JarEntry. |
| * |
| * @param name the jar entry name to look up |
| * @return the JarEntry if it exists, null otherwise |
| */ |
| public JarEntry getJarEntry(String name) |
| { |
| return (JarEntry) getEntry(name); |
| } |
| |
| /** |
| * Returns the manifest for this JarFile or null when the JarFile does not |
| * contain a manifest file. |
| */ |
| public synchronized Manifest getManifest() throws IOException |
| { |
| if (!manifestRead) |
| manifest = readManifest(); |
| |
| return manifest; |
| } |
| |
| // Only called with lock on this JarFile. |
| // Package private for use in inner classes. |
| void readSignatures() throws IOException |
| { |
| Map pkcs7Dsa = new HashMap(); |
| Map pkcs7Rsa = new HashMap(); |
| Map sigFiles = new HashMap(); |
| |
| // Phase 1: Read all signature files. These contain the user |
| // certificates as well as the signatures themselves. |
| for (Enumeration e = super.entries(); e.hasMoreElements(); ) |
| { |
| ZipEntry ze = (ZipEntry) e.nextElement(); |
| String name = ze.getName(); |
| if (name.startsWith(META_INF)) |
| { |
| String alias = name.substring(META_INF.length()); |
| if (alias.lastIndexOf('.') >= 0) |
| alias = alias.substring(0, alias.lastIndexOf('.')); |
| |
| if (name.endsWith(PKCS7_DSA_SUFFIX) || name.endsWith(PKCS7_RSA_SUFFIX)) |
| { |
| if (DEBUG) |
| debug("reading PKCS7 info from " + name + ", alias=" + alias); |
| PKCS7SignedData sig = null; |
| try |
| { |
| sig = new PKCS7SignedData(super.getInputStream(ze)); |
| } |
| catch (CertificateException ce) |
| { |
| IOException ioe = new IOException("certificate parsing error"); |
| ioe.initCause(ce); |
| throw ioe; |
| } |
| catch (CRLException crle) |
| { |
| IOException ioe = new IOException("CRL parsing error"); |
| ioe.initCause(crle); |
| throw ioe; |
| } |
| if (name.endsWith(PKCS7_DSA_SUFFIX)) |
| pkcs7Dsa.put(alias, sig); |
| else if (name.endsWith(PKCS7_RSA_SUFFIX)) |
| pkcs7Rsa.put(alias, sig); |
| } |
| else if (name.endsWith(SF_SUFFIX)) |
| { |
| if (DEBUG) |
| debug("reading signature file for " + alias + ": " + name); |
| Manifest sf = new Manifest(super.getInputStream(ze)); |
| sigFiles.put(alias, sf); |
| if (DEBUG) |
| debug("result: " + sf); |
| } |
| } |
| } |
| |
| // Phase 2: verify the signatures on any signature files. |
| Set validCerts = new HashSet(); |
| Map entryCerts = new HashMap(); |
| for (Iterator it = sigFiles.entrySet().iterator(); it.hasNext(); ) |
| { |
| int valid = 0; |
| Map.Entry e = (Map.Entry) it.next(); |
| String alias = (String) e.getKey(); |
| |
| PKCS7SignedData sig = (PKCS7SignedData) pkcs7Dsa.get(alias); |
| if (sig != null) |
| { |
| Certificate[] certs = sig.getCertificates(); |
| Set signerInfos = sig.getSignerInfos(); |
| for (Iterator it2 = signerInfos.iterator(); it2.hasNext(); ) |
| verify(certs, (SignerInfo) it2.next(), alias, validCerts); |
| } |
| |
| sig = (PKCS7SignedData) pkcs7Rsa.get(alias); |
| if (sig != null) |
| { |
| Certificate[] certs = sig.getCertificates(); |
| Set signerInfos = sig.getSignerInfos(); |
| for (Iterator it2 = signerInfos.iterator(); it2.hasNext(); ) |
| verify(certs, (SignerInfo) it2.next(), alias, validCerts); |
| } |
| |
| // It isn't a signature for anything. Punt it. |
| if (validCerts.isEmpty()) |
| { |
| it.remove(); |
| continue; |
| } |
| |
| entryCerts.put(e.getValue(), new HashSet(validCerts)); |
| validCerts.clear(); |
| } |
| |
| // Phase 3: verify the signature file signatures against the manifest, |
| // mapping the entry name to the target certificates. |
| this.entryCerts = new HashMap(); |
| for (Iterator it = entryCerts.entrySet().iterator(); it.hasNext(); ) |
| { |
| Map.Entry e = (Map.Entry) it.next(); |
| Manifest sigfile = (Manifest) e.getKey(); |
| Map entries = sigfile.getEntries(); |
| Set certificates = (Set) e.getValue(); |
| |
| for (Iterator it2 = entries.entrySet().iterator(); it2.hasNext(); ) |
| { |
| Map.Entry e2 = (Map.Entry) it2.next(); |
| String entryname = String.valueOf(e2.getKey()); |
| Attributes attr = (Attributes) e2.getValue(); |
| if (verifyHashes(entryname, attr)) |
| { |
| if (DEBUG) |
| debug("entry " + entryname + " has certificates " + certificates); |
| Set s = (Set) this.entryCerts.get(entryname); |
| if (s != null) |
| s.addAll(certificates); |
| else |
| this.entryCerts.put(entryname, new HashSet(certificates)); |
| } |
| } |
| } |
| |
| signaturesRead = true; |
| } |
| |
| /** |
| * Tell if the given signer info is over the given alias's signature file, |
| * given one of the certificates specified. |
| */ |
| private void verify(Certificate[] certs, SignerInfo signerInfo, |
| String alias, Set validCerts) |
| { |
| Signature sig = null; |
| try |
| { |
| OID alg = signerInfo.getDigestEncryptionAlgorithmId(); |
| if (alg.equals(DSA_ENCRYPTION_OID)) |
| { |
| if (!signerInfo.getDigestAlgorithmId().equals(SHA1_OID)) |
| return; |
| sig = Signature.getInstance("SHA1withDSA", provider); |
| } |
| else if (alg.equals(RSA_ENCRYPTION_OID)) |
| { |
| OID hash = signerInfo.getDigestAlgorithmId(); |
| if (hash.equals(MD2_OID)) |
| sig = Signature.getInstance("md2WithRsaEncryption", provider); |
| else if (hash.equals(MD4_OID)) |
| sig = Signature.getInstance("md4WithRsaEncryption", provider); |
| else if (hash.equals(MD5_OID)) |
| sig = Signature.getInstance("md5WithRsaEncryption", provider); |
| else if (hash.equals(SHA1_OID)) |
| sig = Signature.getInstance("sha1WithRsaEncryption", provider); |
| else |
| return; |
| } |
| else |
| { |
| if (DEBUG) |
| debug("unsupported signature algorithm: " + alg); |
| return; |
| } |
| } |
| catch (NoSuchAlgorithmException nsae) |
| { |
| if (DEBUG) |
| { |
| debug(nsae); |
| nsae.printStackTrace(); |
| } |
| return; |
| } |
| ZipEntry sigFileEntry = super.getEntry(META_INF + alias + SF_SUFFIX); |
| if (sigFileEntry == null) |
| return; |
| for (int i = 0; i < certs.length; i++) |
| { |
| if (!(certs[i] instanceof X509Certificate)) |
| continue; |
| X509Certificate cert = (X509Certificate) certs[i]; |
| if (!cert.getIssuerX500Principal().equals(signerInfo.getIssuer()) || |
| !cert.getSerialNumber().equals(signerInfo.getSerialNumber())) |
| continue; |
| try |
| { |
| sig.initVerify(cert.getPublicKey()); |
| InputStream in = super.getInputStream(sigFileEntry); |
| if (in == null) |
| continue; |
| byte[] buf = new byte[1024]; |
| int len = 0; |
| while ((len = in.read(buf)) != -1) |
| sig.update(buf, 0, len); |
| if (sig.verify(signerInfo.getEncryptedDigest())) |
| { |
| if (DEBUG) |
| debug("signature for " + cert.getSubjectDN() + " is good"); |
| validCerts.add(cert); |
| } |
| } |
| catch (IOException ioe) |
| { |
| continue; |
| } |
| catch (InvalidKeyException ike) |
| { |
| continue; |
| } |
| catch (SignatureException se) |
| { |
| continue; |
| } |
| } |
| } |
| |
| /** |
| * Verifies that the digest(s) in a signature file were, in fact, made |
| * over the manifest entry for ENTRY. |
| * |
| * @param entry The entry name. |
| * @param attr The attributes from the signature file to verify. |
| */ |
| private boolean verifyHashes(String entry, Attributes attr) |
| { |
| int verified = 0; |
| |
| // The bytes for ENTRY's manifest entry, which are signed in the |
| // signature file. |
| byte[] entryBytes = null; |
| try |
| { |
| ZipEntry e = super.getEntry(entry); |
| if (e == null) |
| { |
| if (DEBUG) |
| debug("verifyHashes: no entry '" + entry + "'"); |
| return false; |
| } |
| entryBytes = readManifestEntry(e); |
| } |
| catch (IOException ioe) |
| { |
| if (DEBUG) |
| { |
| debug(ioe); |
| ioe.printStackTrace(); |
| } |
| return false; |
| } |
| |
| for (Iterator it = attr.entrySet().iterator(); it.hasNext(); ) |
| { |
| Map.Entry e = (Map.Entry) it.next(); |
| String key = String.valueOf(e.getKey()); |
| if (!key.endsWith(DIGEST_KEY_SUFFIX)) |
| continue; |
| String alg = key.substring(0, key.length() - DIGEST_KEY_SUFFIX.length()); |
| try |
| { |
| byte[] hash = Base64InputStream.decode((String) e.getValue()); |
| MessageDigest md = MessageDigest.getInstance(alg, provider); |
| md.update(entryBytes); |
| byte[] hash2 = md.digest(); |
| if (DEBUG) |
| debug("verifying SF entry " + entry + " alg: " + md.getAlgorithm() |
| + " expect=" + new java.math.BigInteger(hash).toString(16) |
| + " comp=" + new java.math.BigInteger(hash2).toString(16)); |
| if (!Arrays.equals(hash, hash2)) |
| return false; |
| verified++; |
| } |
| catch (IOException ioe) |
| { |
| if (DEBUG) |
| { |
| debug(ioe); |
| ioe.printStackTrace(); |
| } |
| return false; |
| } |
| catch (NoSuchAlgorithmException nsae) |
| { |
| if (DEBUG) |
| { |
| debug(nsae); |
| nsae.printStackTrace(); |
| } |
| return false; |
| } |
| } |
| |
| // We have to find at least one valid digest. |
| return verified > 0; |
| } |
| |
| /** |
| * Read the raw bytes that comprise a manifest entry. We can't use the |
| * Manifest object itself, because that loses information (such as line |
| * endings, and order of entries). |
| */ |
| private byte[] readManifestEntry(ZipEntry entry) throws IOException |
| { |
| InputStream in = super.getInputStream(super.getEntry(MANIFEST_NAME)); |
| ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| byte[] target = ("Name: " + entry.getName()).getBytes(); |
| int t = 0, c, prev = -1, state = 0, l = -1; |
| |
| while ((c = in.read()) != -1) |
| { |
| // if (DEBUG) |
| // debug("read " |
| // + (c == '\n' ? "\\n" : (c == '\r' ? "\\r" : String.valueOf((char) c))) |
| // + " state=" + state + " prev=" |
| // + (prev == '\n' ? "\\n" : (prev == '\r' ? "\\r" : String.valueOf((char) prev))) |
| // + " t=" + t + (t < target.length ? (" target[t]=" + (char) target[t]) : "") |
| // + " l=" + l); |
| switch (state) |
| { |
| |
| // Step 1: read until we find the "target" bytes: the start |
| // of the entry we need to read. |
| case 0: |
| if (((byte) c) != target[t]) |
| t = 0; |
| else |
| { |
| t++; |
| if (t == target.length) |
| { |
| out.write(target); |
| state = 1; |
| } |
| } |
| break; |
| |
| // Step 2: assert that there is a newline character after |
| // the "target" bytes. |
| case 1: |
| if (c != '\n' && c != '\r') |
| { |
| out.reset(); |
| t = 0; |
| state = 0; |
| } |
| else |
| { |
| out.write(c); |
| state = 2; |
| } |
| break; |
| |
| // Step 3: read this whole entry, until we reach an empty |
| // line. |
| case 2: |
| if (c == '\n') |
| { |
| out.write(c); |
| // NL always terminates a line. |
| if (l == 0 || (l == 1 && prev == '\r')) |
| return out.toByteArray(); |
| l = 0; |
| } |
| else |
| { |
| // Here we see a blank line terminated by a CR, |
| // followed by the next entry. Technically, `c' should |
| // always be 'N' at this point. |
| if (l == 1 && prev == '\r') |
| return out.toByteArray(); |
| out.write(c); |
| l++; |
| } |
| prev = c; |
| break; |
| |
| default: |
| throw new RuntimeException("this statement should be unreachable"); |
| } |
| } |
| |
| // The last entry, with a single CR terminating the line. |
| if (state == 2 && prev == '\r' && l == 0) |
| return out.toByteArray(); |
| |
| // We should not reach this point, we didn't find the entry (or, possibly, |
| // it is the last entry and is malformed). |
| throw new IOException("could not find " + entry + " in manifest"); |
| } |
| |
| /** |
| * A utility class that verifies jar entries as they are read. |
| */ |
| private static class EntryInputStream extends FilterInputStream |
| { |
| private final JarFile jarfile; |
| private final long length; |
| private long pos; |
| private final ZipEntry entry; |
| private final byte[][] hashes; |
| private final MessageDigest[] md; |
| private boolean checked; |
| |
| EntryInputStream(final ZipEntry entry, |
| final InputStream in, |
| final JarFile jar) |
| throws IOException |
| { |
| super(in); |
| this.entry = entry; |
| this.jarfile = jar; |
| |
| length = entry.getSize(); |
| pos = 0; |
| checked = false; |
| |
| Attributes attr; |
| Manifest manifest = jarfile.getManifest(); |
| if (manifest != null) |
| attr = manifest.getAttributes(entry.getName()); |
| else |
| attr = null; |
| if (DEBUG) |
| debug("verifying entry " + entry + " attr=" + attr); |
| if (attr == null) |
| { |
| hashes = new byte[0][]; |
| md = new MessageDigest[0]; |
| } |
| else |
| { |
| List hashes = new LinkedList(); |
| List md = new LinkedList(); |
| for (Iterator it = attr.entrySet().iterator(); it.hasNext(); ) |
| { |
| Map.Entry e = (Map.Entry) it.next(); |
| String key = String.valueOf(e.getKey()); |
| if (key == null) |
| continue; |
| if (!key.endsWith(DIGEST_KEY_SUFFIX)) |
| continue; |
| hashes.add(Base64InputStream.decode((String) e.getValue())); |
| try |
| { |
| int length = key.length() - DIGEST_KEY_SUFFIX.length(); |
| String alg = key.substring(0, length); |
| md.add(MessageDigest.getInstance(alg, provider)); |
| } |
| catch (NoSuchAlgorithmException nsae) |
| { |
| IOException ioe = new IOException("no such message digest: " + key); |
| ioe.initCause(nsae); |
| throw ioe; |
| } |
| } |
| if (DEBUG) |
| debug("digests=" + md); |
| this.hashes = (byte[][]) hashes.toArray(new byte[hashes.size()][]); |
| this.md = (MessageDigest[]) md.toArray(new MessageDigest[md.size()]); |
| } |
| } |
| |
| public boolean markSupported() |
| { |
| return false; |
| } |
| |
| public void mark(int readLimit) |
| { |
| } |
| |
| public void reset() |
| { |
| } |
| |
| public int read() throws IOException |
| { |
| int b = super.read(); |
| if (b == -1) |
| { |
| eof(); |
| return -1; |
| } |
| for (int i = 0; i < md.length; i++) |
| md[i].update((byte) b); |
| pos++; |
| if (length > 0 && pos >= length) |
| eof(); |
| return b; |
| } |
| |
| public int read(byte[] buf, int off, int len) throws IOException |
| { |
| int count = super.read(buf, off, (int) Math.min(len, (length != 0 |
| ? length - pos |
| : Integer.MAX_VALUE))); |
| if (count == -1 || (length > 0 && pos >= length)) |
| { |
| eof(); |
| return -1; |
| } |
| for (int i = 0; i < md.length; i++) |
| md[i].update(buf, off, count); |
| pos += count; |
| if (length != 0 && pos >= length) |
| eof(); |
| return count; |
| } |
| |
| public int read(byte[] buf) throws IOException |
| { |
| return read(buf, 0, buf.length); |
| } |
| |
| public long skip(long bytes) throws IOException |
| { |
| byte[] b = new byte[1024]; |
| long amount = 0; |
| while (amount < bytes) |
| { |
| int l = read(b, 0, (int) Math.min(b.length, bytes - amount)); |
| if (l == -1) |
| break; |
| amount += l; |
| } |
| return amount; |
| } |
| |
| private void eof() throws IOException |
| { |
| if (checked) |
| return; |
| checked = true; |
| for (int i = 0; i < md.length; i++) |
| { |
| byte[] hash = md[i].digest(); |
| if (DEBUG) |
| debug("verifying " + md[i].getAlgorithm() + " expect=" |
| + new java.math.BigInteger(hashes[i]).toString(16) |
| + " comp=" + new java.math.BigInteger(hash).toString(16)); |
| if (!Arrays.equals(hash, hashes[i])) |
| { |
| synchronized(jarfile) |
| { |
| if (DEBUG) |
| debug(entry + " could NOT be verified"); |
| jarfile.verified.put(entry.getName(), Boolean.FALSE); |
| } |
| return; |
| // XXX ??? what do we do here? |
| // throw new ZipException("message digest mismatch"); |
| } |
| } |
| |
| synchronized(jarfile) |
| { |
| if (DEBUG) |
| debug(entry + " has been VERIFIED"); |
| jarfile.verified.put(entry.getName(), Boolean.TRUE); |
| } |
| } |
| } |
| } |