/* * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package java.util.jar; import java.io.*; import java.lang.ref.SoftReference; import java.net.URL; import java.util.*; import java.util.stream.Stream; import java.util.stream.StreamSupport; import java.util.zip.*; import java.security.CodeSigner; import java.security.cert.Certificate; import java.security.CodeSource; import sun.security.util.ManifestEntryVerifier; import sun.misc.SharedSecrets; import sun.security.util.SignatureFileVerifier; /** * The JarFile class is used to read the contents of a jar file * from any file that can be opened with java.io.RandomAccessFile. * It extends the class java.util.zip.ZipFile with support * for reading an optional Manifest entry. The * Manifest can be used to specify meta-information about the * jar file and its entries. * *

Unless otherwise noted, passing a null argument to a constructor * or method in this class will cause a {@link NullPointerException} to be * thrown. * * If the verify flag is on when opening a signed jar file, the content of the * file is verified against its signature embedded inside the file. Please note * that the verification process does not include validating the signer's * certificate. A caller should inspect the return value of * {@link JarEntry#getCodeSigners()} to further determine if the signature * can be trusted. * * @author David Connelly * @see Manifest * @see java.util.zip.ZipFile * @see java.util.jar.JarEntry * @since 1.2 */ public class JarFile extends ZipFile { private SoftReference manRef; private JarEntry manEntry; private JarVerifier jv; private boolean jvInitialized; private boolean verify; // indicates if Class-Path attribute present (only valid if hasCheckedSpecialAttributes true) private boolean hasClassPathAttribute; // true if manifest checked for special attributes private volatile boolean hasCheckedSpecialAttributes; // Set up JavaUtilJarAccess in SharedSecrets static { SharedSecrets.setJavaUtilJarAccess(new JavaUtilJarAccessImpl()); } private static final sun.misc.JavaUtilZipFileAccess zipAccess = sun.misc.SharedSecrets.getJavaUtilZipFileAccess(); /** * The JAR manifest file name. */ public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; /** * Creates a new JarFile to read from the specified * file name. The JarFile will be verified if * it is signed. * @param name the name of the jar file to be opened for reading * @throws IOException if an I/O error has occurred * @throws SecurityException if access to the file is denied * by the SecurityManager */ public JarFile(String name) throws IOException { this(new File(name), true, ZipFile.OPEN_READ); } /** * Creates a new JarFile to read from the specified * file name. * @param name the name of the jar file to be opened for reading * @param verify whether or not to verify the jar file if * it is signed. * @throws IOException if an I/O error has occurred * @throws SecurityException if access to the file is denied * by the SecurityManager */ public JarFile(String name, boolean verify) throws IOException { this(new File(name), verify, ZipFile.OPEN_READ); } /** * Creates a new JarFile to read from the specified * File object. The JarFile will be verified if * it is signed. * @param file the jar file to be opened for reading * @throws IOException if an I/O error has occurred * @throws SecurityException if access to the file is denied * by the SecurityManager */ public JarFile(File file) throws IOException { this(file, true, ZipFile.OPEN_READ); } /** * Creates a new JarFile to read from the specified * File object. * @param file the jar file to be opened for reading * @param verify whether or not to verify the jar file if * it is signed. * @throws IOException if an I/O error has occurred * @throws SecurityException if access to the file is denied * by the SecurityManager. */ public JarFile(File file, boolean verify) throws IOException { this(file, verify, ZipFile.OPEN_READ); } /** * Creates a new JarFile to read from the specified * File object in the specified mode. The mode argument * must be either OPEN_READ or OPEN_READ | OPEN_DELETE. * * @param file the jar file to be opened for reading * @param verify whether or not to verify the jar file if * it is signed. * @param mode the mode in which the file is to be opened * @throws IOException if an I/O error has occurred * @throws IllegalArgumentException * if the mode argument is invalid * @throws SecurityException if access to the file is denied * by the SecurityManager * @since 1.3 */ public JarFile(File file, boolean verify, int mode) throws IOException { super(file, mode); this.verify = verify; } /** * Returns the jar file manifest, or null if none. * * @return the jar file manifest, or null if none * * @throws IllegalStateException * may be thrown if the jar file has been closed * @throws IOException if an I/O error has occurred */ public Manifest getManifest() throws IOException { return getManifestFromReference(); } private Manifest getManifestFromReference() throws IOException { Manifest man = manRef != null ? manRef.get() : null; if (man == null) { JarEntry entry = getManEntry(); // If found then load the manifest if (entry != null) { byte[] b = zipAccess.getBytes(this, entry); man = new Manifest(new ByteArrayInputStream(b)); if (verify && !jvInitialized) { jv = new JarVerifier(b); } manRef = new SoftReference<>(man); } } return man; } private native String[] getMetaInfEntryNames(); /** * Returns the JarEntry for the given entry name or * null if not found. * * @param name the jar file entry name * @return the JarEntry for the given entry name or * null if not found. * * @throws IllegalStateException * may be thrown if the jar file has been closed * * @see java.util.jar.JarEntry */ public JarEntry getJarEntry(String name) { return (JarEntry)getEntry(name); } /** * Returns the ZipEntry for the given entry name or * null if not found. * * @param name the jar file entry name * @return the ZipEntry for the given entry name or * null if not found * * @throws IllegalStateException * may be thrown if the jar file has been closed * * @see java.util.zip.ZipEntry */ public ZipEntry getEntry(String name) { ZipEntry ze = super.getEntry(name); if (ze != null) { return new JarFileEntry(ze); } return null; } private class JarEntryIterator implements Enumeration, Iterator { final Enumeration e = JarFile.super.entries(); public boolean hasNext() { return e.hasMoreElements(); } public JarEntry next() { ZipEntry ze = e.nextElement(); return new JarFileEntry(ze); } public boolean hasMoreElements() { return hasNext(); } public JarEntry nextElement() { return next(); } } /** * Returns an enumeration of the zip file entries. */ public Enumeration entries() { return new JarEntryIterator(); } @Override public Stream stream() { return StreamSupport.stream(Spliterators.spliterator( new JarEntryIterator(), size(), Spliterator.ORDERED | Spliterator.DISTINCT | Spliterator.IMMUTABLE | Spliterator.NONNULL), false); } private class JarFileEntry extends JarEntry { JarFileEntry(ZipEntry ze) { super(ze); } public Attributes getAttributes() throws IOException { Manifest man = JarFile.this.getManifest(); if (man != null) { return man.getAttributes(getName()); } else { return null; } } public Certificate[] getCertificates() { try { maybeInstantiateVerifier(); } catch (IOException e) { throw new RuntimeException(e); } if (certs == null && jv != null) { certs = jv.getCerts(JarFile.this, this); } return certs == null ? null : certs.clone(); } public CodeSigner[] getCodeSigners() { try { maybeInstantiateVerifier(); } catch (IOException e) { throw new RuntimeException(e); } if (signers == null && jv != null) { signers = jv.getCodeSigners(JarFile.this, this); } return signers == null ? null : signers.clone(); } } /* * Ensures that the JarVerifier has been created if one is * necessary (i.e., the jar appears to be signed.) This is done as * a quick check to avoid processing of the manifest for unsigned * jars. */ private void maybeInstantiateVerifier() throws IOException { if (jv != null) { return; } if (verify) { String[] names = getMetaInfEntryNames(); if (names != null) { for (String nameLower : names) { String name = nameLower.toUpperCase(Locale.ENGLISH); if (name.endsWith(".DSA") || name.endsWith(".RSA") || name.endsWith(".EC") || name.endsWith(".SF")) { // Assume since we found a signature-related file // that the jar is signed and that we therefore // need a JarVerifier and Manifest getManifest(); return; } } } // No signature-related files; don't instantiate a // verifier verify = false; } } /* * Initializes the verifier object by reading all the manifest * entries and passing them to the verifier. */ private void initializeVerifier() { ManifestEntryVerifier mev = null; // Verify "META-INF/" entries... try { String[] names = getMetaInfEntryNames(); if (names != null) { for (String name : names) { String uname = name.toUpperCase(Locale.ENGLISH); if (MANIFEST_NAME.equals(uname) || SignatureFileVerifier.isBlockOrSF(uname)) { JarEntry e = getJarEntry(name); if (e == null) { throw new JarException("corrupted jar file"); } if (mev == null) { mev = new ManifestEntryVerifier (getManifestFromReference()); } byte[] b = zipAccess.getBytes(this, e); if (b != null && b.length > 0) { jv.beginEntry(e, mev); jv.update(b.length, b, 0, b.length, mev); jv.update(-1, null, 0, 0, mev); } } } } } catch (IOException ex) { // if we had an error parsing any blocks, just // treat the jar file as being unsigned jv = null; verify = false; if (JarVerifier.debug != null) { JarVerifier.debug.println("jarfile parsing error!"); ex.printStackTrace(); } } // if after initializing the verifier we have nothing // signed, we null it out. if (jv != null) { jv.doneWithMeta(); if (JarVerifier.debug != null) { JarVerifier.debug.println("done with meta!"); } if (jv.nothingToVerify()) { if (JarVerifier.debug != null) { JarVerifier.debug.println("nothing to verify!"); } jv = null; verify = false; } } } /** * Returns an input stream for reading the contents of the specified * zip file entry. * @param ze the zip file entry * @return an input stream for reading the contents of the specified * zip file entry * @throws ZipException if a zip file format error has occurred * @throws IOException if an I/O error has occurred * @throws SecurityException if any of the jar file entries * are incorrectly signed. * @throws IllegalStateException * may be thrown if the jar file has been closed */ public synchronized InputStream getInputStream(ZipEntry ze) throws IOException { maybeInstantiateVerifier(); if (jv == null) { return super.getInputStream(ze); } if (!jvInitialized) { initializeVerifier(); jvInitialized = true; // could be set to null after a call to // initializeVerifier if we have nothing to // verify if (jv == null) return super.getInputStream(ze); } // wrap a verifier stream around the real stream return new JarVerifier.VerifierStream( getManifestFromReference(), ze instanceof JarFileEntry ? (JarEntry) ze : getJarEntry(ze.getName()), super.getInputStream(ze), jv); } // Statics for hand-coded Boyer-Moore search private static final char[] CLASSPATH_CHARS = {'c','l','a','s','s','-','p','a','t','h'}; // The bad character shift for "class-path" private static final int[] CLASSPATH_LASTOCC; // The good suffix shift for "class-path" private static final int[] CLASSPATH_OPTOSFT; static { CLASSPATH_LASTOCC = new int[128]; CLASSPATH_OPTOSFT = new int[10]; CLASSPATH_LASTOCC[(int)'c'] = 1; CLASSPATH_LASTOCC[(int)'l'] = 2; CLASSPATH_LASTOCC[(int)'s'] = 5; CLASSPATH_LASTOCC[(int)'-'] = 6; CLASSPATH_LASTOCC[(int)'p'] = 7; CLASSPATH_LASTOCC[(int)'a'] = 8; CLASSPATH_LASTOCC[(int)'t'] = 9; CLASSPATH_LASTOCC[(int)'h'] = 10; for (int i=0; i<9; i++) CLASSPATH_OPTOSFT[i] = 10; CLASSPATH_OPTOSFT[9]=1; } private JarEntry getManEntry() { if (manEntry == null) { // First look up manifest entry using standard name manEntry = getJarEntry(MANIFEST_NAME); if (manEntry == null) { // If not found, then iterate through all the "META-INF/" // entries to find a match. String[] names = getMetaInfEntryNames(); if (names != null) { for (String name : names) { if (MANIFEST_NAME.equals(name.toUpperCase(Locale.ENGLISH))) { manEntry = getJarEntry(name); break; } } } } } return manEntry; } /** * Returns {@code true} iff this JAR file has a manifest with the * Class-Path attribute */ boolean hasClassPathAttribute() throws IOException { checkForSpecialAttributes(); return hasClassPathAttribute; } /** * Returns true if the pattern {@code src} is found in {@code b}. * The {@code lastOcc} and {@code optoSft} arrays are the precomputed * bad character and good suffix shifts. */ private boolean match(char[] src, byte[] b, int[] lastOcc, int[] optoSft) { int len = src.length; int last = b.length - len; int i = 0; next: while (i<=last) { for (int j=(len-1); j>=0; j--) { char c = (char) b[i+j]; c = (((c-'A')|('Z'-c)) >= 0) ? (char)(c + 32) : c; if (c != src[j]) { i += Math.max(j + 1 - lastOcc[c&0x7F], optoSft[j]); continue next; } } return true; } return false; } /** * On first invocation, check if the JAR file has the Class-Path * attribute. A no-op on subsequent calls. */ private void checkForSpecialAttributes() throws IOException { if (hasCheckedSpecialAttributes) return; JarEntry entry = getManEntry(); if (entry != null) { byte[] b = zipAccess.getBytes(this, entry); if (match(CLASSPATH_CHARS, b, CLASSPATH_LASTOCC, CLASSPATH_OPTOSFT)) hasClassPathAttribute = true; } hasCheckedSpecialAttributes = true; } private synchronized void ensureInitialization() { try { maybeInstantiateVerifier(); } catch (IOException e) { throw new RuntimeException(e); } if (jv != null && !jvInitialized) { initializeVerifier(); jvInitialized = true; } } JarEntry newEntry(ZipEntry ze) { return new JarFileEntry(ze); } Enumeration entryNames(CodeSource[] cs) { ensureInitialization(); if (jv != null) { return jv.entryNames(this, cs); } /* * JAR file has no signed content. Is there a non-signing * code source? */ boolean includeUnsigned = false; for (CodeSource c : cs) { if (c.getCodeSigners() == null) { includeUnsigned = true; break; } } if (includeUnsigned) { return unsignedEntryNames(); } else { return new Enumeration<>() { public boolean hasMoreElements() { return false; } public String nextElement() { throw new NoSuchElementException(); } }; } } /** * Returns an enumeration of the zip file entries * excluding internal JAR mechanism entries and including * signed entries missing from the ZIP directory. */ Enumeration entries2() { ensureInitialization(); if (jv != null) { return jv.entries2(this, super.entries()); } // screen out entries which are never signed final Enumeration enum_ = super.entries(); return new Enumeration<>() { ZipEntry entry; public boolean hasMoreElements() { if (entry != null) { return true; } while (enum_.hasMoreElements()) { ZipEntry ze = enum_.nextElement(); if (JarVerifier.isSigningRelated(ze.getName())) { continue; } entry = ze; return true; } return false; } public JarFileEntry nextElement() { if (hasMoreElements()) { ZipEntry ze = entry; entry = null; return new JarFileEntry(ze); } throw new NoSuchElementException(); } }; } CodeSource[] getCodeSources(URL url) { ensureInitialization(); if (jv != null) { return jv.getCodeSources(this, url); } /* * JAR file has no signed content. Is there a non-signing * code source? */ Enumeration unsigned = unsignedEntryNames(); if (unsigned.hasMoreElements()) { return new CodeSource[]{JarVerifier.getUnsignedCS(url)}; } else { return null; } } private Enumeration unsignedEntryNames() { final Enumeration entries = entries(); return new Enumeration<>() { String name; /* * Grab entries from ZIP directory but screen out * metadata. */ public boolean hasMoreElements() { if (name != null) { return true; } while (entries.hasMoreElements()) { String value; ZipEntry e = entries.nextElement(); value = e.getName(); if (e.isDirectory() || JarVerifier.isSigningRelated(value)) { continue; } name = value; return true; } return false; } public String nextElement() { if (hasMoreElements()) { String value = name; name = null; return value; } throw new NoSuchElementException(); } }; } CodeSource getCodeSource(URL url, String name) { ensureInitialization(); if (jv != null) { if (jv.eagerValidation) { CodeSource cs = null; JarEntry je = getJarEntry(name); if (je != null) { cs = jv.getCodeSource(url, this, je); } else { cs = jv.getCodeSource(url, name); } return cs; } else { return jv.getCodeSource(url, name); } } return JarVerifier.getUnsignedCS(url); } void setEagerValidation(boolean eager) { try { maybeInstantiateVerifier(); } catch (IOException e) { throw new RuntimeException(e); } if (jv != null) { jv.setEagerValidation(eager); } } List getManifestDigests() { ensureInitialization(); if (jv != null) { return jv.getManifestDigests(); } return new ArrayList<>(); } }