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