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