Home | History | Annotate | Download | only in jar
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 
     18 package android.util.jar;
     19 
     20 import android.system.ErrnoException;
     21 import android.system.Os;
     22 import android.system.OsConstants;
     23 
     24 import dalvik.system.CloseGuard;
     25 import java.io.FileDescriptor;
     26 import java.io.FilterInputStream;
     27 import java.io.IOException;
     28 import java.io.InputStream;
     29 import java.security.cert.Certificate;
     30 import java.util.HashMap;
     31 import java.util.Iterator;
     32 import java.util.Set;
     33 import java.util.jar.JarFile;
     34 import java.util.zip.Inflater;
     35 import java.util.zip.InflaterInputStream;
     36 import java.util.zip.ZipEntry;
     37 import libcore.io.IoBridge;
     38 import libcore.io.IoUtils;
     39 import libcore.io.Streams;
     40 
     41 /**
     42  * A subset of the JarFile API implemented as a thin wrapper over
     43  * system/core/libziparchive.
     44  *
     45  * @hide for internal use only. Not API compatible (or as forgiving) as
     46  *        {@link java.util.jar.JarFile}
     47  */
     48 public final class StrictJarFile {
     49 
     50     private final long nativeHandle;
     51 
     52     // NOTE: It's possible to share a file descriptor with the native
     53     // code, at the cost of some additional complexity.
     54     private final FileDescriptor fd;
     55 
     56     private final StrictJarManifest manifest;
     57     private final StrictJarVerifier verifier;
     58 
     59     private final boolean isSigned;
     60 
     61     private final CloseGuard guard = CloseGuard.get();
     62     private boolean closed;
     63 
     64     public StrictJarFile(String fileName)
     65             throws IOException, SecurityException {
     66         this(fileName, true, true);
     67     }
     68 
     69     public StrictJarFile(FileDescriptor fd)
     70             throws IOException, SecurityException {
     71         this(fd, true, true);
     72     }
     73 
     74     public StrictJarFile(FileDescriptor fd,
     75             boolean verify,
     76             boolean signatureSchemeRollbackProtectionsEnforced)
     77                     throws IOException, SecurityException {
     78         this("[fd:" + fd.getInt$() + "]", fd, verify,
     79                 signatureSchemeRollbackProtectionsEnforced);
     80     }
     81 
     82     public StrictJarFile(String fileName,
     83             boolean verify,
     84             boolean signatureSchemeRollbackProtectionsEnforced)
     85                     throws IOException, SecurityException {
     86         this(fileName, IoBridge.open(fileName, OsConstants.O_RDONLY),
     87                 verify, signatureSchemeRollbackProtectionsEnforced);
     88     }
     89 
     90     /**
     91      * @param name of the archive (not necessarily a path).
     92      * @param fd seekable file descriptor for the JAR file.
     93      * @param verify whether to verify the file's JAR signatures and collect the corresponding
     94      *        signer certificates.
     95      * @param signatureSchemeRollbackProtectionsEnforced {@code true} to enforce protections against
     96      *        stripping newer signature schemes (e.g., APK Signature Scheme v2) from the file, or
     97      *        {@code false} to ignore any such protections. This parameter is ignored when
     98      *        {@code verify} is {@code false}.
     99      */
    100     private StrictJarFile(String name,
    101             FileDescriptor fd,
    102             boolean verify,
    103             boolean signatureSchemeRollbackProtectionsEnforced)
    104                     throws IOException, SecurityException {
    105         this.nativeHandle = nativeOpenJarFile(name, fd.getInt$());
    106         this.fd = fd;
    107 
    108         try {
    109             // Read the MANIFEST and signature files up front and try to
    110             // parse them. We never want to accept a JAR File with broken signatures
    111             // or manifests, so it's best to throw as early as possible.
    112             if (verify) {
    113                 HashMap<String, byte[]> metaEntries = getMetaEntries();
    114                 this.manifest = new StrictJarManifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
    115                 this.verifier =
    116                         new StrictJarVerifier(
    117                                 name,
    118                                 manifest,
    119                                 metaEntries,
    120                                 signatureSchemeRollbackProtectionsEnforced);
    121                 Set<String> files = manifest.getEntries().keySet();
    122                 for (String file : files) {
    123                     if (findEntry(file) == null) {
    124                         throw new SecurityException("File " + file + " in manifest does not exist");
    125                     }
    126                 }
    127 
    128                 isSigned = verifier.readCertificates() && verifier.isSignedJar();
    129             } else {
    130                 isSigned = false;
    131                 this.manifest = null;
    132                 this.verifier = null;
    133             }
    134         } catch (IOException | SecurityException e) {
    135             nativeClose(this.nativeHandle);
    136             IoUtils.closeQuietly(fd);
    137             closed = true;
    138             throw e;
    139         }
    140 
    141         guard.open("close");
    142     }
    143 
    144     public StrictJarManifest getManifest() {
    145         return manifest;
    146     }
    147 
    148     public Iterator<ZipEntry> iterator() throws IOException {
    149         return new EntryIterator(nativeHandle, "");
    150     }
    151 
    152     public ZipEntry findEntry(String name) {
    153         return nativeFindEntry(nativeHandle, name);
    154     }
    155 
    156     /**
    157      * Return all certificate chains for a given {@link ZipEntry} belonging to this jar.
    158      * This method MUST be called only after fully exhausting the InputStream belonging
    159      * to this entry.
    160      *
    161      * Returns {@code null} if this jar file isn't signed or if this method is
    162      * called before the stream is processed.
    163      */
    164     public Certificate[][] getCertificateChains(ZipEntry ze) {
    165         if (isSigned) {
    166             return verifier.getCertificateChains(ze.getName());
    167         }
    168 
    169         return null;
    170     }
    171 
    172     /**
    173      * Return all certificates for a given {@link ZipEntry} belonging to this jar.
    174      * This method MUST be called only after fully exhausting the InputStream belonging
    175      * to this entry.
    176      *
    177      * Returns {@code null} if this jar file isn't signed or if this method is
    178      * called before the stream is processed.
    179      *
    180      * @deprecated Switch callers to use getCertificateChains instead
    181      */
    182     @Deprecated
    183     public Certificate[] getCertificates(ZipEntry ze) {
    184         if (isSigned) {
    185             Certificate[][] certChains = verifier.getCertificateChains(ze.getName());
    186 
    187             // Measure number of certs.
    188             int count = 0;
    189             for (Certificate[] chain : certChains) {
    190                 count += chain.length;
    191             }
    192 
    193             // Create new array and copy all the certs into it.
    194             Certificate[] certs = new Certificate[count];
    195             int i = 0;
    196             for (Certificate[] chain : certChains) {
    197                 System.arraycopy(chain, 0, certs, i, chain.length);
    198                 i += chain.length;
    199             }
    200 
    201             return certs;
    202         }
    203 
    204         return null;
    205     }
    206 
    207     public InputStream getInputStream(ZipEntry ze) {
    208         final InputStream is = getZipInputStream(ze);
    209 
    210         if (isSigned) {
    211             StrictJarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName());
    212             if (entry == null) {
    213                 return is;
    214             }
    215 
    216             return new JarFileInputStream(is, ze.getSize(), entry);
    217         }
    218 
    219         return is;
    220     }
    221 
    222     public void close() throws IOException {
    223         if (!closed) {
    224             if (guard != null) {
    225                 guard.close();
    226             }
    227 
    228             nativeClose(nativeHandle);
    229             IoUtils.closeQuietly(fd);
    230             closed = true;
    231         }
    232     }
    233 
    234     @Override
    235     protected void finalize() throws Throwable {
    236         try {
    237             if (guard != null) {
    238                 guard.warnIfOpen();
    239             }
    240             close();
    241         } finally {
    242             super.finalize();
    243         }
    244     }
    245 
    246     private InputStream getZipInputStream(ZipEntry ze) {
    247         if (ze.getMethod() == ZipEntry.STORED) {
    248             return new FDStream(fd, ze.getDataOffset(),
    249                     ze.getDataOffset() + ze.getSize());
    250         } else {
    251             final FDStream wrapped = new FDStream(
    252                     fd, ze.getDataOffset(), ze.getDataOffset() + ze.getCompressedSize());
    253 
    254             int bufSize = Math.max(1024, (int) Math.min(ze.getSize(), 65535L));
    255             return new ZipInflaterInputStream(wrapped, new Inflater(true), bufSize, ze);
    256         }
    257     }
    258 
    259     static final class EntryIterator implements Iterator<ZipEntry> {
    260         private final long iterationHandle;
    261         private ZipEntry nextEntry;
    262 
    263         EntryIterator(long nativeHandle, String prefix) throws IOException {
    264             iterationHandle = nativeStartIteration(nativeHandle, prefix);
    265         }
    266 
    267         public ZipEntry next() {
    268             if (nextEntry != null) {
    269                 final ZipEntry ze = nextEntry;
    270                 nextEntry = null;
    271                 return ze;
    272             }
    273 
    274             return nativeNextEntry(iterationHandle);
    275         }
    276 
    277         public boolean hasNext() {
    278             if (nextEntry != null) {
    279                 return true;
    280             }
    281 
    282             final ZipEntry ze = nativeNextEntry(iterationHandle);
    283             if (ze == null) {
    284                 return false;
    285             }
    286 
    287             nextEntry = ze;
    288             return true;
    289         }
    290 
    291         public void remove() {
    292             throw new UnsupportedOperationException();
    293         }
    294     }
    295 
    296     private HashMap<String, byte[]> getMetaEntries() throws IOException {
    297         HashMap<String, byte[]> metaEntries = new HashMap<String, byte[]>();
    298 
    299         Iterator<ZipEntry> entryIterator = new EntryIterator(nativeHandle, "META-INF/");
    300         while (entryIterator.hasNext()) {
    301             final ZipEntry entry = entryIterator.next();
    302             metaEntries.put(entry.getName(), Streams.readFully(getInputStream(entry)));
    303         }
    304 
    305         return metaEntries;
    306     }
    307 
    308     static final class JarFileInputStream extends FilterInputStream {
    309         private final StrictJarVerifier.VerifierEntry entry;
    310 
    311         private long count;
    312         private boolean done = false;
    313 
    314         JarFileInputStream(InputStream is, long size, StrictJarVerifier.VerifierEntry e) {
    315             super(is);
    316             entry = e;
    317 
    318             count = size;
    319         }
    320 
    321         @Override
    322         public int read() throws IOException {
    323             if (done) {
    324                 return -1;
    325             }
    326             if (count > 0) {
    327                 int r = super.read();
    328                 if (r != -1) {
    329                     entry.write(r);
    330                     count--;
    331                 } else {
    332                     count = 0;
    333                 }
    334                 if (count == 0) {
    335                     done = true;
    336                     entry.verify();
    337                 }
    338                 return r;
    339             } else {
    340                 done = true;
    341                 entry.verify();
    342                 return -1;
    343             }
    344         }
    345 
    346         @Override
    347         public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
    348             if (done) {
    349                 return -1;
    350             }
    351             if (count > 0) {
    352                 int r = super.read(buffer, byteOffset, byteCount);
    353                 if (r != -1) {
    354                     int size = r;
    355                     if (count < size) {
    356                         size = (int) count;
    357                     }
    358                     entry.write(buffer, byteOffset, size);
    359                     count -= size;
    360                 } else {
    361                     count = 0;
    362                 }
    363                 if (count == 0) {
    364                     done = true;
    365                     entry.verify();
    366                 }
    367                 return r;
    368             } else {
    369                 done = true;
    370                 entry.verify();
    371                 return -1;
    372             }
    373         }
    374 
    375         @Override
    376         public int available() throws IOException {
    377             if (done) {
    378                 return 0;
    379             }
    380             return super.available();
    381         }
    382 
    383         @Override
    384         public long skip(long byteCount) throws IOException {
    385             return Streams.skipByReading(this, byteCount);
    386         }
    387     }
    388 
    389     /** @hide */
    390     public static class ZipInflaterInputStream extends InflaterInputStream {
    391         private final ZipEntry entry;
    392         private long bytesRead = 0;
    393 
    394         public ZipInflaterInputStream(InputStream is, Inflater inf, int bsize, ZipEntry entry) {
    395             super(is, inf, bsize);
    396             this.entry = entry;
    397         }
    398 
    399         @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
    400             final int i;
    401             try {
    402                 i = super.read(buffer, byteOffset, byteCount);
    403             } catch (IOException e) {
    404                 throw new IOException("Error reading data for " + entry.getName() + " near offset "
    405                         + bytesRead, e);
    406             }
    407             if (i == -1) {
    408                 if (entry.getSize() != bytesRead) {
    409                     throw new IOException("Size mismatch on inflated file: " + bytesRead + " vs "
    410                             + entry.getSize());
    411                 }
    412             } else {
    413                 bytesRead += i;
    414             }
    415             return i;
    416         }
    417 
    418         @Override public int available() throws IOException {
    419             if (closed) {
    420                 // Our superclass will throw an exception, but there's a jtreg test that
    421                 // explicitly checks that the InputStream returned from ZipFile.getInputStream
    422                 // returns 0 even when closed.
    423                 return 0;
    424             }
    425             return super.available() == 0 ? 0 : (int) (entry.getSize() - bytesRead);
    426         }
    427     }
    428 
    429     /**
    430      * Wrap a stream around a FileDescriptor.  The file descriptor is shared
    431      * among all streams returned by getInputStream(), so we have to synchronize
    432      * access to it.  (We can optimize this by adding buffering here to reduce
    433      * collisions.)
    434      *
    435      * <p>We could support mark/reset, but we don't currently need them.
    436      *
    437      * @hide
    438      */
    439     public static class FDStream extends InputStream {
    440         private final FileDescriptor fd;
    441         private long endOffset;
    442         private long offset;
    443 
    444         public FDStream(FileDescriptor fd, long initialOffset, long endOffset) {
    445             this.fd = fd;
    446             offset = initialOffset;
    447             this.endOffset = endOffset;
    448         }
    449 
    450         @Override public int available() throws IOException {
    451             return (offset < endOffset ? 1 : 0);
    452         }
    453 
    454         @Override public int read() throws IOException {
    455             return Streams.readSingleByte(this);
    456         }
    457 
    458         @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
    459             synchronized (this.fd) {
    460                 final long length = endOffset - offset;
    461                 if (byteCount > length) {
    462                     byteCount = (int) length;
    463                 }
    464                 try {
    465                     Os.lseek(fd, offset, OsConstants.SEEK_SET);
    466                 } catch (ErrnoException e) {
    467                     throw new IOException(e);
    468                 }
    469                 int count = IoBridge.read(fd, buffer, byteOffset, byteCount);
    470                 if (count > 0) {
    471                     offset += count;
    472                     return count;
    473                 } else {
    474                     return -1;
    475                 }
    476             }
    477         }
    478 
    479         @Override public long skip(long byteCount) throws IOException {
    480             if (byteCount > endOffset - offset) {
    481                 byteCount = endOffset - offset;
    482             }
    483             offset += byteCount;
    484             return byteCount;
    485         }
    486     }
    487 
    488     private static native long nativeOpenJarFile(String name, int fd)
    489             throws IOException;
    490     private static native long nativeStartIteration(long nativeHandle, String prefix);
    491     private static native ZipEntry nativeNextEntry(long iterationHandle);
    492     private static native ZipEntry nativeFindEntry(long nativeHandle, String entryName);
    493     private static native void nativeClose(long nativeHandle);
    494 }
    495