blob: 11aee2f540e6a0ebb00fd61f1c44e44024f30d39 [file] [log] [blame]
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +00001/*
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
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +090020import android.system.ErrnoException;
21import android.system.Os;
22import android.system.OsConstants;
23
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +000024import dalvik.system.CloseGuard;
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +090025import java.io.FileDescriptor;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +000026import java.io.FilterInputStream;
27import java.io.IOException;
28import java.io.InputStream;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +000029import java.security.cert.Certificate;
30import java.util.HashMap;
31import java.util.Iterator;
32import java.util.Set;
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +090033import java.util.jar.JarFile;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +000034import java.util.zip.Inflater;
35import java.util.zip.InflaterInputStream;
36import java.util.zip.ZipEntry;
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +090037import libcore.io.IoBridge;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +000038import 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.
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +090054 private final FileDescriptor fd;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +000055
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
Alex Klyubine4157182016-01-05 13:27:05 -080064 public StrictJarFile(String fileName)
65 throws IOException, SecurityException {
Alex Klyubin9b59bc42016-03-24 12:02:20 -070066 this(fileName, true, true);
Alex Klyubine4157182016-01-05 13:27:05 -080067 }
68
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +090069 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
Alex Klyubin9b59bc42016-03-24 12:02:20 -070090 /**
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +090091 * @param name of the archive (not necessarily a path).
92 * @param fd seekable file descriptor for the JAR file.
Alex Klyubin9b59bc42016-03-24 12:02:20 -070093 * @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 */
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +0900100 private StrictJarFile(String name,
101 FileDescriptor fd,
Alex Klyubin9b59bc42016-03-24 12:02:20 -0700102 boolean verify,
103 boolean signatureSchemeRollbackProtectionsEnforced)
104 throws IOException, SecurityException {
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +0900105 this.nativeHandle = nativeOpenJarFile(name, fd.getInt$());
106 this.fd = fd;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000107
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.
Alex Klyubine4157182016-01-05 13:27:05 -0800112 if (verify) {
113 HashMap<String, byte[]> metaEntries = getMetaEntries();
114 this.manifest = new StrictJarManifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
Alex Klyubin9b59bc42016-03-24 12:02:20 -0700115 this.verifier =
116 new StrictJarVerifier(
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +0900117 name,
Alex Klyubin9b59bc42016-03-24 12:02:20 -0700118 manifest,
119 metaEntries,
120 signatureSchemeRollbackProtectionsEnforced);
Alex Klyubine4157182016-01-05 13:27:05 -0800121 Set<String> files = manifest.getEntries().keySet();
122 for (String file : files) {
123 if (findEntry(file) == null) {
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +0900124 throw new SecurityException("File " + file + " in manifest does not exist");
Alex Klyubine4157182016-01-05 13:27:05 -0800125 }
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000126 }
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000127
Alex Klyubine4157182016-01-05 13:27:05 -0800128 isSigned = verifier.readCertificates() && verifier.isSignedJar();
129 } else {
130 isSigned = false;
131 this.manifest = null;
132 this.verifier = null;
133 }
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000134 } catch (IOException | SecurityException e) {
135 nativeClose(this.nativeHandle);
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +0900136 IoUtils.closeQuietly(fd);
Tomasz Mikolajewskib061fc22016-12-06 10:05:05 +0900137 closed = true;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000138 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) {
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +0900224 if (guard != null) {
225 guard.close();
226 }
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000227
228 nativeClose(nativeHandle);
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +0900229 IoUtils.closeQuietly(fd);
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000230 closed = true;
231 }
232 }
233
Neil Fullerb69f6142015-12-03 17:31:18 +0000234 @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
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000246 private InputStream getZipInputStream(ZipEntry ze) {
247 if (ze.getMethod() == ZipEntry.STORED) {
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +0900248 return new FDStream(fd, ze.getDataOffset(),
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000249 ze.getDataOffset() + ze.getSize());
250 } else {
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +0900251 final FDStream wrapped = new FDStream(
252 fd, ze.getDataOffset(), ze.getDataOffset() + ze.getCompressedSize());
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000253
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;
Paul Duffin96a13682018-07-17 16:14:42 +0100393 private boolean closed;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000394
395 public ZipInflaterInputStream(InputStream is, Inflater inf, int bsize, ZipEntry entry) {
396 super(is, inf, bsize);
397 this.entry = entry;
398 }
399
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
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 }
Paul Duffin96a13682018-07-17 16:14:42 +0100428
429 @Override
430 public void close() throws IOException {
431 super.close();
432 closed = true;
433 }
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000434 }
435
436 /**
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +0900437 * Wrap a stream around a FileDescriptor. The file descriptor is shared
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000438 * 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 */
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +0900446 public static class FDStream extends InputStream {
447 private final FileDescriptor fd;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000448 private long endOffset;
449 private long offset;
450
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +0900451 public FDStream(FileDescriptor fd, long initialOffset, long endOffset) {
452 this.fd = fd;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000453 offset = initialOffset;
454 this.endOffset = endOffset;
455 }
456
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000457 @Override public int available() throws IOException {
458 return (offset < endOffset ? 1 : 0);
459 }
460
461 @Override public int read() throws IOException {
462 return Streams.readSingleByte(this);
463 }
464
465 @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +0900466 synchronized (this.fd) {
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000467 final long length = endOffset - offset;
468 if (byteCount > length) {
469 byteCount = (int) length;
470 }
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +0900471 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);
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000477 if (count > 0) {
478 offset += count;
479 return count;
480 } else {
481 return -1;
482 }
483 }
484 }
485
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
Tomasz Mikolajewski6c3df152016-10-20 10:49:53 +0900495 private static native long nativeOpenJarFile(String name, int fd)
496 throws IOException;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000497 private static native long nativeStartIteration(long nativeHandle, String prefix);
498 private static native ZipEntry nativeNextEntry(long iterationHandle);
499 private static native ZipEntry nativeFindEntry(long nativeHandle, String entryName);
500 private static native void nativeClose(long nativeHandle);
501}