blob: 5c29d74f92a1eaf2f30f03821a7292cc7eb0f6e3 [file]
/*
* Copyright (c) 1997, 2024, 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.util.zip.*;
import java.io.*;
import sun.security.util.ManifestEntryVerifier;
/**
* The {@code JarInputStream} class, which extends {@link ZipInputStream},
* is used to read the contents of a JAR file from an input stream.
* It provides support for reading an optional
* <a href="{@docRoot}/../specs/jar/jar.html#jar-manifest">Manifest</a>
* entry. The {@code Manifest} can be used to store
* meta-information about the JAR file and its entries.
* <p>
* Unless otherwise noted, passing a {@code null} argument to a constructor
* or method in this class will cause a {@link NullPointerException} to be
* thrown.
* </p>
* <h2>Accessing the Manifest</h2>
* <p>
* The {@link #getManifest() getManifest} method is used to return the
* <a href="{@docRoot}/../specs/jar/jar.html#jar-manifest">Manifest</a>
* from the entry {@code META-INF/MANIFEST.MF} when it is the first entry
* in the stream (or the second entry if the first entry in the stream is
* {@code META-INF/} and the second entry is {@code META-INF/MANIFEST.MF}).
* </p>
* <p> The {@link #getNextJarEntry()} and {@link #getNextEntry()} methods are
* used to read JAR file entries from the stream. These methods skip over the
* Manifest ({@code META-INF/MANIFEST.MF}) when it is at the beginning of the
* stream. In other words, these methods do not return an entry for the Manifest
* when the Manifest is the first entry in the stream. If the first entry is
* {@code META-INF/} and the second entry is the Manifest then both are skipped
* over by these methods. Whether these methods skip over the Manifest when it
* appears later in the stream is not specified.
* </p>
* <h2>Signed JAR Files</h2>
*
* A {@code JarInputStream} verifies the signatures of entries in a
* <a href="{@docRoot}/../specs/jar/jar.html#signed-jar-file">Signed JAR file</a>
* when:
* <ul>
* <li>
* The {@code Manifest} is the first entry in the stream (or the second
* entry if the first entry in the stream is {@code META-INF/} and the
* second entry is {@code META-INF/MANIFEST.MF}).
* </li>
* <li>
* All signature-related entries immediately follow the {@code Manifest}
* </li>
* </ul>
* <p>
* Once the {@code JarEntry} has been completely verified, which is done by
* reading until the end of the entry's input stream,
* {@link JarEntry#getCertificates()} may be called to obtain the certificates
* for this entry and {@link JarEntry#getCodeSigners()} may be called to obtain
* the signers.
* </p>
* <p>
* It is important to 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.
* </p>
* @apiNote
* If a {@code JarEntry} is modified after the JAR file is signed,
* a {@link SecurityException} will be thrown when the entry is read.
*
* @author David Connelly
* @see Manifest
* @see java.util.zip.ZipInputStream
* @since 1.2
*/
public class JarInputStream extends ZipInputStream {
private Manifest man;
private JarEntry first;
private JarVerifier jv;
private ManifestEntryVerifier mev;
private final boolean doVerify;
private boolean tryManifest;
/**
* Creates a new {@code JarInputStream} and reads the optional
* manifest. If a manifest is present, also attempts to verify
* the signatures if the JarInputStream is signed.
* @param in the actual input stream
* @throws IOException if an I/O error has occurred
*/
public JarInputStream(InputStream in) throws IOException {
this(in, true);
}
/**
* Creates a new {@code JarInputStream} and reads the optional
* manifest. If a manifest is present and verify is true, also attempts
* to verify the signatures if the JarInputStream is signed.
*
* @param in the actual input stream
* @param verify whether or not to verify the JarInputStream if
* it is signed.
* @throws IOException if an I/O error has occurred
*/
@SuppressWarnings("this-escape")
public JarInputStream(InputStream in, boolean verify) throws IOException {
super(in);
this.doVerify = verify;
// This implementation assumes the META-INF/MANIFEST.MF entry
// should be either the first or the second entry (when preceded
// by the dir META-INF/). It skips the META-INF/ and then
// "consumes" the MANIFEST.MF to initialize the Manifest object.
JarEntry e = (JarEntry)super.getNextEntry();
if (e != null && e.getName().equalsIgnoreCase("META-INF/"))
e = (JarEntry)super.getNextEntry();
first = checkManifest(e);
}
private JarEntry checkManifest(JarEntry e)
throws IOException
{
if (e != null && JarFile.MANIFEST_NAME.equalsIgnoreCase(e.getName())) {
man = new Manifest();
byte[] bytes = readAllBytes();
man.read(new ByteArrayInputStream(bytes));
closeEntry();
if (doVerify) {
jv = new JarVerifier(e.getName(), bytes);
mev = new ManifestEntryVerifier(man, jv.manifestName);
}
JarEntry nextEntry = (JarEntry)super.getNextEntry();
if (nextEntry != null &&
JarFile.MANIFEST_NAME.equalsIgnoreCase(nextEntry.getName())) {
if (JarVerifier.debug != null) {
JarVerifier.debug.println(JarVerifier.MULTIPLE_MANIFEST_WARNING);
}
jv = null;
mev = null;
}
return nextEntry;
}
return e;
}
/**
* Returns the {@code Manifest} for this JAR file when it is the first entry
* in the stream (or the second entry if the first entry in the stream is
* {@code META-INF/} and the second entry is {@code META-INF/MANIFEST.MF}), or
* {@code null} otherwise.
*
* @return the {@code Manifest} for this JAR file, or
* {@code null} otherwise.
*/
public Manifest getManifest() {
return man;
}
/**
* Reads the next ZIP file entry and positions the stream at the
* beginning of the entry data. If verification has been enabled,
* any invalid signature detected while positioning the stream for
* the next entry will result in an exception.
* @throws ZipException if a ZIP file error has occurred
* @throws IOException if an I/O error has occurred
* @throws SecurityException if any of the jar file entries
* are incorrectly signed.
*/
public ZipEntry getNextEntry() throws IOException {
JarEntry e;
if (first == null) {
e = (JarEntry)super.getNextEntry();
if (tryManifest) {
e = checkManifest(e);
tryManifest = false;
}
} else {
e = first;
if (first.getName().equalsIgnoreCase(JarFile.INDEX_NAME))
tryManifest = true;
first = null;
}
if (jv != null && e != null) {
// At this point, we might have parsed all the meta-inf
// entries and have nothing to verify. If we have
// nothing to verify, get rid of the JarVerifier object.
if (jv.nothingToVerify() == true) {
jv = null;
mev = null;
} else {
jv.beginEntry(e, mev);
}
}
return e;
}
/**
* Reads the next JAR file entry and positions the stream at the
* beginning of the entry data. If verification has been enabled,
* any invalid signature detected while positioning the stream for
* the next entry will result in an exception.
* @return the next JAR file entry, or null if there are no more entries
* @throws ZipException if a ZIP file error has occurred
* @throws IOException if an I/O error has occurred
* @throws SecurityException if any of the jar file entries
* are incorrectly signed.
*/
public JarEntry getNextJarEntry() throws IOException {
return (JarEntry)getNextEntry();
}
/**
* Reads from the current JAR entry into an array of bytes, returning the number of
* inflated bytes. If {@code len} is not zero, the method blocks until some input is
* available; otherwise, no bytes are read and {@code 0} is returned.
* <p>
* If the current entry is compressed and this method returns a nonzero
* integer <i>n</i> then {@code buf[off]}
* through {@code buf[off+}<i>n</i>{@code -1]} contain the uncompressed
* data. The content of elements {@code buf[off+}<i>n</i>{@code ]} through
* {@code buf[off+}<i>len</i>{@code -1]} is undefined, contrary to the
* specification of the {@link java.io.InputStream InputStream} superclass,
* so an implementation is free to modify these elements during the inflate
* operation. If this method returns {@code -1} or throws an exception then
* the content of {@code buf[off]} through {@code buf[off+}<i>len</i>{@code
* -1]} is undefined.
* <p>
* If verification has been enabled, any invalid signature
* on the current entry will be reported at some point before the
* end of the entry is reached.
* @param b the buffer into which the data is read
* @param off the start offset in the destination array {@code b}
* @param len the maximum number of bytes to read
* @return the actual number of bytes read, or -1 if the end of the
* entry is reached
* @throws IndexOutOfBoundsException If {@code off} is negative,
* {@code len} is negative, or {@code len} is greater than
* {@code b.length - off}
* @throws ZipException if a ZIP file error has occurred
* @throws IOException if an I/O error has occurred
* @throws SecurityException if any of the jar file entries
* are incorrectly signed.
*/
public int read(byte[] b, int off, int len) throws IOException {
int n;
if (first == null) {
n = super.read(b, off, len);
} else {
n = -1;
}
if (jv != null) {
jv.update(n, b, off, len, mev);
}
return n;
}
/**
* Creates a new {@code JarEntry} ({@code ZipEntry}) for the
* specified JAR file entry name. The manifest attributes of
* the specified JAR file entry name will be copied to the new
* <CODE>JarEntry</CODE>.
*
* @param name the name of the JAR/ZIP file entry
* @return the {@code JarEntry} object just created
*/
protected ZipEntry createZipEntry(String name) {
JarEntry e = new JarEntry(name);
if (man != null) {
e.attr = man.getAttributes(name);
}
return e;
}
}