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
18package android.util.jar;
19
20import android.system.ErrnoException;
21import android.system.Os;
22import android.system.OsConstants;
23
24import dalvik.system.CloseGuard;
25import java.io.FileDescriptor;
26import java.io.FilterInputStream;
27import java.io.IOException;
28import java.io.InputStream;
29import java.security.cert.Certificate;
30import java.util.HashMap;
31import java.util.Iterator;
32import java.util.Set;
33import java.util.jar.JarFile;
34import java.util.zip.Inflater;
35import java.util.zip.InflaterInputStream;
36import java.util.zip.ZipEntry;
37import libcore.io.IoBridge;
38import libcore.io.IoUtils;
39import 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 */
48public 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