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 
StrictJarFile(String fileName)64     public StrictJarFile(String fileName)
65             throws IOException, SecurityException {
66         this(fileName, true, true);
67     }
68 
StrictJarFile(FileDescriptor fd)69     public StrictJarFile(FileDescriptor fd)
70             throws IOException, SecurityException {
71         this(fd, true, true);
72     }
73 
StrictJarFile(FileDescriptor fd, boolean verify, boolean signatureSchemeRollbackProtectionsEnforced)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 
StrictJarFile(String fileName, boolean verify, boolean signatureSchemeRollbackProtectionsEnforced)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      */
StrictJarFile(String name, FileDescriptor fd, boolean verify, boolean signatureSchemeRollbackProtectionsEnforced)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 
getManifest()144     public StrictJarManifest getManifest() {
145         return manifest;
146     }
147 
iterator()148     public Iterator<ZipEntry> iterator() throws IOException {
149         return new EntryIterator(nativeHandle, "");
150     }
151 
findEntry(String name)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      */
getCertificateChains(ZipEntry ze)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
getCertificates(ZipEntry ze)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 
getInputStream(ZipEntry ze)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 
close()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
finalize()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 
getZipInputStream(ZipEntry ze)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 
EntryIterator(long nativeHandle, String prefix)263         EntryIterator(long nativeHandle, String prefix) throws IOException {
264             iterationHandle = nativeStartIteration(nativeHandle, prefix);
265         }
266 
next()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 
hasNext()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 
remove()291         public void remove() {
292             throw new UnsupportedOperationException();
293         }
294     }
295 
getMetaEntries()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 
JarFileInputStream(InputStream is, long size, StrictJarVerifier.VerifierEntry e)314         JarFileInputStream(InputStream is, long size, StrictJarVerifier.VerifierEntry e) {
315             super(is);
316             entry = e;
317 
318             count = size;
319         }
320 
321         @Override
read()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
read(byte[] buffer, int byteOffset, int byteCount)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
available()376         public int available() throws IOException {
377             if (done) {
378                 return 0;
379             }
380             return super.available();
381         }
382 
383         @Override
skip(long byteCount)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         private boolean closed;
394 
ZipInflaterInputStream(InputStream is, Inflater inf, int bsize, ZipEntry entry)395         public ZipInflaterInputStream(InputStream is, Inflater inf, int bsize, ZipEntry entry) {
396             super(is, inf, bsize);
397             this.entry = entry;
398         }
399 
read(byte[] buffer, int byteOffset, int byteCount)400         @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
401             final int i;
402             try {
403                 i = super.read(buffer, byteOffset, byteCount);
404             } catch (IOException e) {
405                 throw new IOException("Error reading data for " + entry.getName() + " near offset "
406                         + bytesRead, e);
407             }
408             if (i == -1) {
409                 if (entry.getSize() != bytesRead) {
410                     throw new IOException("Size mismatch on inflated file: " + bytesRead + " vs "
411                             + entry.getSize());
412                 }
413             } else {
414                 bytesRead += i;
415             }
416             return i;
417         }
418 
available()419         @Override public int available() throws IOException {
420             if (closed) {
421                 // Our superclass will throw an exception, but there's a jtreg test that
422                 // explicitly checks that the InputStream returned from ZipFile.getInputStream
423                 // returns 0 even when closed.
424                 return 0;
425             }
426             return super.available() == 0 ? 0 : (int) (entry.getSize() - bytesRead);
427         }
428 
429         @Override
close()430         public void close() throws IOException {
431             super.close();
432             closed = true;
433         }
434     }
435 
436     /**
437      * Wrap a stream around a FileDescriptor.  The file descriptor is shared
438      * among all streams returned by getInputStream(), so we have to synchronize
439      * access to it.  (We can optimize this by adding buffering here to reduce
440      * collisions.)
441      *
442      * <p>We could support mark/reset, but we don't currently need them.
443      *
444      * @hide
445      */
446     public static class FDStream extends InputStream {
447         private final FileDescriptor fd;
448         private long endOffset;
449         private long offset;
450 
FDStream(FileDescriptor fd, long initialOffset, long endOffset)451         public FDStream(FileDescriptor fd, long initialOffset, long endOffset) {
452             this.fd = fd;
453             offset = initialOffset;
454             this.endOffset = endOffset;
455         }
456 
available()457         @Override public int available() throws IOException {
458             return (offset < endOffset ? 1 : 0);
459         }
460 
read()461         @Override public int read() throws IOException {
462             return Streams.readSingleByte(this);
463         }
464 
read(byte[] buffer, int byteOffset, int byteCount)465         @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
466             synchronized (this.fd) {
467                 final long length = endOffset - offset;
468                 if (byteCount > length) {
469                     byteCount = (int) length;
470                 }
471                 try {
472                     Os.lseek(fd, offset, OsConstants.SEEK_SET);
473                 } catch (ErrnoException e) {
474                     throw new IOException(e);
475                 }
476                 int count = IoBridge.read(fd, buffer, byteOffset, byteCount);
477                 if (count > 0) {
478                     offset += count;
479                     return count;
480                 } else {
481                     return -1;
482                 }
483             }
484         }
485 
skip(long byteCount)486         @Override public long skip(long byteCount) throws IOException {
487             if (byteCount > endOffset - offset) {
488                 byteCount = endOffset - offset;
489             }
490             offset += byteCount;
491             return byteCount;
492         }
493     }
494 
nativeOpenJarFile(String name, int fd)495     private static native long nativeOpenJarFile(String name, int fd)
496             throws IOException;
nativeStartIteration(long nativeHandle, String prefix)497     private static native long nativeStartIteration(long nativeHandle, String prefix);
nativeNextEntry(long iterationHandle)498     private static native ZipEntry nativeNextEntry(long iterationHandle);
nativeFindEntry(long nativeHandle, String entryName)499     private static native ZipEntry nativeFindEntry(long nativeHandle, String entryName);
nativeClose(long nativeHandle)500     private static native void nativeClose(long nativeHandle);
501 }
502