[go: nahoru, domu]

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