From 6a8ad6d161d6846311267f5e4aa933b8490bc821 Mon Sep 17 00:00:00 2001 From: Przemyslaw Szczepaniak Date: Tue, 3 Nov 2015 09:47:56 +0000 Subject: [PATCH] Move StrictJarFile from libcore to framework Bug: 25337946 (cherry picked from commit 8a7c1606d88873c5a1b5764c16cb046b6f2275b2) Change-Id: I1bfce4129887d7cbfc02d92641b44920d7cdbbee --- core/java/android/content/pm/PackageParser.java | 3 +- core/java/android/util/jar/StrictJarFile.java | 427 +++++++++++++++++++ core/java/android/util/jar/StrictJarManifest.java | 315 ++++++++++++++ .../android/util/jar/StrictJarManifestReader.java | 184 +++++++++ core/java/android/util/jar/StrictJarVerifier.java | 456 +++++++++++++++++++++ core/jni/Android.mk | 1 + core/jni/AndroidRuntime.cpp | 4 + core/jni/android_util_jar_StrictJarFile.cpp | 172 ++++++++ preloaded-classes | 11 +- 9 files changed, 1569 insertions(+), 4 deletions(-) create mode 100644 core/java/android/util/jar/StrictJarFile.java create mode 100644 core/java/android/util/jar/StrictJarManifest.java create mode 100644 core/java/android/util/jar/StrictJarManifestReader.java create mode 100644 core/java/android/util/jar/StrictJarVerifier.java create mode 100644 core/jni/android_util_jar_StrictJarFile.cpp diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 99bd39035358..549829dbf841 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -81,9 +81,10 @@ import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; -import java.util.jar.StrictJarFile; import java.util.zip.ZipEntry; +import android.util.jar.StrictJarFile; + /** * Parser for package files (APKs) on disk. This supports apps packaged either * as a single "monolithic" APK, or apps packaged as a "cluster" of multiple diff --git a/core/java/android/util/jar/StrictJarFile.java b/core/java/android/util/jar/StrictJarFile.java new file mode 100644 index 000000000000..fd5780612649 --- /dev/null +++ b/core/java/android/util/jar/StrictJarFile.java @@ -0,0 +1,427 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.util.jar; + +import dalvik.system.CloseGuard; +import java.io.ByteArrayInputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.security.cert.Certificate; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Set; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import libcore.io.IoUtils; +import libcore.io.Streams; + +/** + * A subset of the JarFile API implemented as a thin wrapper over + * system/core/libziparchive. + * + * @hide for internal use only. Not API compatible (or as forgiving) as + * {@link java.util.jar.JarFile} + */ +public final class StrictJarFile { + + private final long nativeHandle; + + // NOTE: It's possible to share a file descriptor with the native + // code, at the cost of some additional complexity. + private final RandomAccessFile raf; + + private final StrictJarManifest manifest; + private final StrictJarVerifier verifier; + + private final boolean isSigned; + + private final CloseGuard guard = CloseGuard.get(); + private boolean closed; + + public StrictJarFile(String fileName) throws IOException, SecurityException { + this.nativeHandle = nativeOpenJarFile(fileName); + this.raf = new RandomAccessFile(fileName, "r"); + + try { + // Read the MANIFEST and signature files up front and try to + // parse them. We never want to accept a JAR File with broken signatures + // or manifests, so it's best to throw as early as possible. + HashMap metaEntries = getMetaEntries(); + this.manifest = new StrictJarManifest(metaEntries.get(JarFile.MANIFEST_NAME), true); + this.verifier = new StrictJarVerifier(fileName, manifest, metaEntries); + Set files = manifest.getEntries().keySet(); + for (String file : files) { + if (findEntry(file) == null) { + throw new SecurityException(fileName + ": File " + file + " in manifest does not exist"); + } + } + + isSigned = verifier.readCertificates() && verifier.isSignedJar(); + } catch (IOException | SecurityException e) { + nativeClose(this.nativeHandle); + IoUtils.closeQuietly(this.raf); + throw e; + } + + guard.open("close"); + } + + public StrictJarManifest getManifest() { + return manifest; + } + + public Iterator iterator() throws IOException { + return new EntryIterator(nativeHandle, ""); + } + + public ZipEntry findEntry(String name) { + return nativeFindEntry(nativeHandle, name); + } + + /** + * Return all certificate chains for a given {@link ZipEntry} belonging to this jar. + * This method MUST be called only after fully exhausting the InputStream belonging + * to this entry. + * + * Returns {@code null} if this jar file isn't signed or if this method is + * called before the stream is processed. + */ + public Certificate[][] getCertificateChains(ZipEntry ze) { + if (isSigned) { + return verifier.getCertificateChains(ze.getName()); + } + + return null; + } + + /** + * Return all certificates for a given {@link ZipEntry} belonging to this jar. + * This method MUST be called only after fully exhausting the InputStream belonging + * to this entry. + * + * Returns {@code null} if this jar file isn't signed or if this method is + * called before the stream is processed. + * + * @deprecated Switch callers to use getCertificateChains instead + */ + @Deprecated + public Certificate[] getCertificates(ZipEntry ze) { + if (isSigned) { + Certificate[][] certChains = verifier.getCertificateChains(ze.getName()); + + // Measure number of certs. + int count = 0; + for (Certificate[] chain : certChains) { + count += chain.length; + } + + // Create new array and copy all the certs into it. + Certificate[] certs = new Certificate[count]; + int i = 0; + for (Certificate[] chain : certChains) { + System.arraycopy(chain, 0, certs, i, chain.length); + i += chain.length; + } + + return certs; + } + + return null; + } + + public InputStream getInputStream(ZipEntry ze) { + final InputStream is = getZipInputStream(ze); + + if (isSigned) { + StrictJarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName()); + if (entry == null) { + return is; + } + + return new JarFileInputStream(is, ze.getSize(), entry); + } + + return is; + } + + public void close() throws IOException { + if (!closed) { + guard.close(); + + nativeClose(nativeHandle); + IoUtils.closeQuietly(raf); + closed = true; + } + } + + private InputStream getZipInputStream(ZipEntry ze) { + if (ze.getMethod() == ZipEntry.STORED) { + return new RAFStream(raf, ze.getDataOffset(), + ze.getDataOffset() + ze.getSize()); + } else { + final RAFStream wrapped = new RAFStream( + raf, ze.getDataOffset(), ze.getDataOffset() + ze.getCompressedSize()); + + int bufSize = Math.max(1024, (int) Math.min(ze.getSize(), 65535L)); + return new ZipInflaterInputStream(wrapped, new Inflater(true), bufSize, ze); + } + } + + static final class EntryIterator implements Iterator { + private final long iterationHandle; + private ZipEntry nextEntry; + + EntryIterator(long nativeHandle, String prefix) throws IOException { + iterationHandle = nativeStartIteration(nativeHandle, prefix); + } + + public ZipEntry next() { + if (nextEntry != null) { + final ZipEntry ze = nextEntry; + nextEntry = null; + return ze; + } + + return nativeNextEntry(iterationHandle); + } + + public boolean hasNext() { + if (nextEntry != null) { + return true; + } + + final ZipEntry ze = nativeNextEntry(iterationHandle); + if (ze == null) { + return false; + } + + nextEntry = ze; + return true; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private HashMap getMetaEntries() throws IOException { + HashMap metaEntries = new HashMap(); + + Iterator entryIterator = new EntryIterator(nativeHandle, "META-INF/"); + while (entryIterator.hasNext()) { + final ZipEntry entry = entryIterator.next(); + metaEntries.put(entry.getName(), Streams.readFully(getInputStream(entry))); + } + + return metaEntries; + } + + static final class JarFileInputStream extends FilterInputStream { + private final StrictJarVerifier.VerifierEntry entry; + + private long count; + private boolean done = false; + + JarFileInputStream(InputStream is, long size, StrictJarVerifier.VerifierEntry e) { + super(is); + entry = e; + + count = size; + } + + @Override + public int read() throws IOException { + if (done) { + return -1; + } + if (count > 0) { + int r = super.read(); + if (r != -1) { + entry.write(r); + count--; + } else { + count = 0; + } + if (count == 0) { + done = true; + entry.verify(); + } + return r; + } else { + done = true; + entry.verify(); + return -1; + } + } + + @Override + public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { + if (done) { + return -1; + } + if (count > 0) { + int r = super.read(buffer, byteOffset, byteCount); + if (r != -1) { + int size = r; + if (count < size) { + size = (int) count; + } + entry.write(buffer, byteOffset, size); + count -= size; + } else { + count = 0; + } + if (count == 0) { + done = true; + entry.verify(); + } + return r; + } else { + done = true; + entry.verify(); + return -1; + } + } + + @Override + public int available() throws IOException { + if (done) { + return 0; + } + return super.available(); + } + + @Override + public long skip(long byteCount) throws IOException { + return Streams.skipByReading(this, byteCount); + } + } + + /** @hide */ + public static class ZipInflaterInputStream extends InflaterInputStream { + private final ZipEntry entry; + private long bytesRead = 0; + + public ZipInflaterInputStream(InputStream is, Inflater inf, int bsize, ZipEntry entry) { + super(is, inf, bsize); + this.entry = entry; + } + + @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { + final int i; + try { + i = super.read(buffer, byteOffset, byteCount); + } catch (IOException e) { + throw new IOException("Error reading data for " + entry.getName() + " near offset " + + bytesRead, e); + } + if (i == -1) { + if (entry.getSize() != bytesRead) { + throw new IOException("Size mismatch on inflated file: " + bytesRead + " vs " + + entry.getSize()); + } + } else { + bytesRead += i; + } + return i; + } + + @Override public int available() throws IOException { + if (closed) { + // Our superclass will throw an exception, but there's a jtreg test that + // explicitly checks that the InputStream returned from ZipFile.getInputStream + // returns 0 even when closed. + return 0; + } + return super.available() == 0 ? 0 : (int) (entry.getSize() - bytesRead); + } + } + + /** + * Wrap a stream around a RandomAccessFile. The RandomAccessFile is shared + * among all streams returned by getInputStream(), so we have to synchronize + * access to it. (We can optimize this by adding buffering here to reduce + * collisions.) + * + *

We could support mark/reset, but we don't currently need them. + * + * @hide + */ + public static class RAFStream extends InputStream { + private final RandomAccessFile sharedRaf; + private long endOffset; + private long offset; + + + public RAFStream(RandomAccessFile raf, long initialOffset, long endOffset) { + sharedRaf = raf; + offset = initialOffset; + this.endOffset = endOffset; + } + + public RAFStream(RandomAccessFile raf, long initialOffset) throws IOException { + this(raf, initialOffset, raf.length()); + } + + @Override public int available() throws IOException { + return (offset < endOffset ? 1 : 0); + } + + @Override public int read() throws IOException { + return Streams.readSingleByte(this); + } + + @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { + synchronized (sharedRaf) { + final long length = endOffset - offset; + if (byteCount > length) { + byteCount = (int) length; + } + sharedRaf.seek(offset); + int count = sharedRaf.read(buffer, byteOffset, byteCount); + if (count > 0) { + offset += count; + return count; + } else { + return -1; + } + } + } + + @Override public long skip(long byteCount) throws IOException { + if (byteCount > endOffset - offset) { + byteCount = endOffset - offset; + } + offset += byteCount; + return byteCount; + } + } + + + private static native long nativeOpenJarFile(String fileName) throws IOException; + private static native long nativeStartIteration(long nativeHandle, String prefix); + private static native ZipEntry nativeNextEntry(long iterationHandle); + private static native ZipEntry nativeFindEntry(long nativeHandle, String entryName); + private static native void nativeClose(long nativeHandle); +} diff --git a/core/java/android/util/jar/StrictJarManifest.java b/core/java/android/util/jar/StrictJarManifest.java new file mode 100644 index 000000000000..dbb466cd1760 --- /dev/null +++ b/core/java/android/util/jar/StrictJarManifest.java @@ -0,0 +1,315 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util.jar; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.jar.Attributes; +import libcore.io.Streams; + +/** + * The {@code StrictJarManifest} class is used to obtain attribute information for a + * {@code StrictJarFile} and its entries. + * + * @hide + */ +public class StrictJarManifest implements Cloneable { + static final int LINE_LENGTH_LIMIT = 72; + + private static final byte[] LINE_SEPARATOR = new byte[] { '\r', '\n' }; + + private static final byte[] VALUE_SEPARATOR = new byte[] { ':', ' ' }; + + private final Attributes mainAttributes; + private final HashMap entries; + + static final class Chunk { + final int start; + final int end; + + Chunk(int start, int end) { + this.start = start; + this.end = end; + } + } + + private HashMap chunks; + + /** + * The end of the main attributes section in the manifest is needed in + * verification. + */ + private int mainEnd; + + /** + * Creates a new {@code StrictJarManifest} instance. + */ + public StrictJarManifest() { + entries = new HashMap(); + mainAttributes = new Attributes(); + } + + /** + * Creates a new {@code StrictJarManifest} instance using the attributes obtained + * from the input stream. + * + * @param is + * {@code InputStream} to parse for attributes. + * @throws IOException + * if an IO error occurs while creating this {@code StrictJarManifest} + */ + public StrictJarManifest(InputStream is) throws IOException { + this(); + read(Streams.readFully(is)); + } + + /** + * Creates a new {@code StrictJarManifest} instance. The new instance will have the + * same attributes as those found in the parameter {@code StrictJarManifest}. + * + * @param man + * {@code StrictJarManifest} instance to obtain attributes from. + */ + @SuppressWarnings("unchecked") + public StrictJarManifest(StrictJarManifest man) { + mainAttributes = (Attributes) man.mainAttributes.clone(); + entries = (HashMap) ((HashMap) man + .getEntries()).clone(); + } + + StrictJarManifest(byte[] manifestBytes, boolean readChunks) throws IOException { + this(); + if (readChunks) { + chunks = new HashMap(); + } + read(manifestBytes); + } + + /** + * Resets the both the main attributes as well as the entry attributes + * associated with this {@code StrictJarManifest}. + */ + public void clear() { + entries.clear(); + mainAttributes.clear(); + } + + /** + * Returns the {@code Attributes} associated with the parameter entry + * {@code name}. + * + * @param name + * the name of the entry to obtain {@code Attributes} from. + * @return the Attributes for the entry or {@code null} if the entry does + * not exist. + */ + public Attributes getAttributes(String name) { + return getEntries().get(name); + } + + /** + * Returns a map containing the {@code Attributes} for each entry in the + * {@code StrictJarManifest}. + * + * @return the map of entry attributes. + */ + public Map getEntries() { + return entries; + } + + /** + * Returns the main {@code Attributes} of the {@code JarFile}. + * + * @return main {@code Attributes} associated with the source {@code + * JarFile}. + */ + public Attributes getMainAttributes() { + return mainAttributes; + } + + /** + * Creates a copy of this {@code StrictJarManifest}. The returned {@code StrictJarManifest} + * will equal the {@code StrictJarManifest} from which it was cloned. + * + * @return a copy of this instance. + */ + @Override + public Object clone() { + return new StrictJarManifest(this); + } + + /** + * Writes this {@code StrictJarManifest}'s name/attributes pairs to the given {@code OutputStream}. + * The {@code MANIFEST_VERSION} or {@code SIGNATURE_VERSION} attribute must be set before + * calling this method, or no attributes will be written. + * + * @throws IOException + * If an error occurs writing the {@code StrictJarManifest}. + */ + public void write(OutputStream os) throws IOException { + write(this, os); + } + + /** + * Merges name/attribute pairs read from the input stream {@code is} into this manifest. + * + * @param is + * The {@code InputStream} to read from. + * @throws IOException + * If an error occurs reading the manifest. + */ + public void read(InputStream is) throws IOException { + read(Streams.readFullyNoClose(is)); + } + + private void read(byte[] buf) throws IOException { + if (buf.length == 0) { + return; + } + + StrictJarManifestReader im = new StrictJarManifestReader(buf, mainAttributes); + mainEnd = im.getEndOfMainSection(); + im.readEntries(entries, chunks); + } + + /** + * Returns the hash code for this instance. + * + * @return this {@code StrictJarManifest}'s hashCode. + */ + @Override + public int hashCode() { + return mainAttributes.hashCode() ^ getEntries().hashCode(); + } + + /** + * Determines if the receiver is equal to the parameter object. Two {@code + * StrictJarManifest}s are equal if they have identical main attributes as well as + * identical entry attributes. + * + * @param o + * the object to compare against. + * @return {@code true} if the manifests are equal, {@code false} otherwise + */ + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + if (o.getClass() != this.getClass()) { + return false; + } + if (!mainAttributes.equals(((StrictJarManifest) o).mainAttributes)) { + return false; + } + return getEntries().equals(((StrictJarManifest) o).getEntries()); + } + + Chunk getChunk(String name) { + return chunks.get(name); + } + + void removeChunks() { + chunks = null; + } + + int getMainAttributesEnd() { + return mainEnd; + } + + /** + * Writes out the attribute information of the specified manifest to the + * specified {@code OutputStream} + * + * @param manifest + * the manifest to write out. + * @param out + * The {@code OutputStream} to write to. + * @throws IOException + * If an error occurs writing the {@code StrictJarManifest}. + */ + static void write(StrictJarManifest manifest, OutputStream out) throws IOException { + CharsetEncoder encoder = StandardCharsets.UTF_8.newEncoder(); + ByteBuffer buffer = ByteBuffer.allocate(LINE_LENGTH_LIMIT); + + Attributes.Name versionName = Attributes.Name.MANIFEST_VERSION; + String version = manifest.mainAttributes.getValue(versionName); + if (version == null) { + versionName = Attributes.Name.SIGNATURE_VERSION; + version = manifest.mainAttributes.getValue(versionName); + } + if (version != null) { + writeEntry(out, versionName, version, encoder, buffer); + Iterator entries = manifest.mainAttributes.keySet().iterator(); + while (entries.hasNext()) { + Attributes.Name name = (Attributes.Name) entries.next(); + if (!name.equals(versionName)) { + writeEntry(out, name, manifest.mainAttributes.getValue(name), encoder, buffer); + } + } + } + out.write(LINE_SEPARATOR); + Iterator i = manifest.getEntries().keySet().iterator(); + while (i.hasNext()) { + String key = i.next(); + writeEntry(out, Attributes.Name.NAME, key, encoder, buffer); + Attributes attributes = manifest.entries.get(key); + Iterator entries = attributes.keySet().iterator(); + while (entries.hasNext()) { + Attributes.Name name = (Attributes.Name) entries.next(); + writeEntry(out, name, attributes.getValue(name), encoder, buffer); + } + out.write(LINE_SEPARATOR); + } + } + + private static void writeEntry(OutputStream os, Attributes.Name name, + String value, CharsetEncoder encoder, ByteBuffer bBuf) throws IOException { + String nameString = name.toString(); + os.write(nameString.getBytes(StandardCharsets.US_ASCII)); + os.write(VALUE_SEPARATOR); + + encoder.reset(); + bBuf.clear().limit(LINE_LENGTH_LIMIT - nameString.length() - 2); + + CharBuffer cBuf = CharBuffer.wrap(value); + + while (true) { + CoderResult r = encoder.encode(cBuf, bBuf, true); + if (CoderResult.UNDERFLOW == r) { + r = encoder.flush(bBuf); + } + os.write(bBuf.array(), bBuf.arrayOffset(), bBuf.position()); + os.write(LINE_SEPARATOR); + if (CoderResult.UNDERFLOW == r) { + break; + } + os.write(' '); + bBuf.clear().limit(LINE_LENGTH_LIMIT - 1); + } + } +} diff --git a/core/java/android/util/jar/StrictJarManifestReader.java b/core/java/android/util/jar/StrictJarManifestReader.java new file mode 100644 index 000000000000..9881bb003d03 --- /dev/null +++ b/core/java/android/util/jar/StrictJarManifestReader.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util.jar; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.Attributes; + +/** + * Reads a JAR file manifest. The specification is here: + * http://java.sun.com/javase/6/docs/technotes/guides/jar/jar.html + */ +class StrictJarManifestReader { + // There are relatively few unique attribute names, + // but a manifest might have thousands of entries. + private final HashMap attributeNameCache = new HashMap(); + + private final ByteArrayOutputStream valueBuffer = new ByteArrayOutputStream(80); + + private final byte[] buf; + + private final int endOfMainSection; + + private int pos; + + private Attributes.Name name; + + private String value; + + private int consecutiveLineBreaks = 0; + + public StrictJarManifestReader(byte[] buf, Attributes main) throws IOException { + this.buf = buf; + while (readHeader()) { + main.put(name, value); + } + this.endOfMainSection = pos; + } + + public void readEntries(Map entries, Map chunks) throws IOException { + int mark = pos; + while (readHeader()) { + if (!Attributes.Name.NAME.equals(name)) { + throw new IOException("Entry is not named"); + } + String entryNameValue = value; + + Attributes entry = entries.get(entryNameValue); + if (entry == null) { + entry = new Attributes(12); + } + + while (readHeader()) { + entry.put(name, value); + } + + if (chunks != null) { + if (chunks.get(entryNameValue) != null) { + // TODO A bug: there might be several verification chunks for + // the same name. I believe they should be used to update + // signature in order of appearance; there are two ways to fix + // this: either use a list of chunks, or decide on used + // signature algorithm in advance and reread the chunks while + // updating the signature; for now a defensive error is thrown + throw new IOException("A jar verifier does not support more than one entry with the same name"); + } + chunks.put(entryNameValue, new StrictJarManifest.Chunk(mark, pos)); + mark = pos; + } + + entries.put(entryNameValue, entry); + } + } + + public int getEndOfMainSection() { + return endOfMainSection; + } + + /** + * Read a single line from the manifest buffer. + */ + private boolean readHeader() throws IOException { + if (consecutiveLineBreaks > 1) { + // break a section on an empty line + consecutiveLineBreaks = 0; + return false; + } + readName(); + consecutiveLineBreaks = 0; + readValue(); + // if the last line break is missed, the line + // is ignored by the reference implementation + return consecutiveLineBreaks > 0; + } + + private void readName() throws IOException { + int mark = pos; + + while (pos < buf.length) { + if (buf[pos++] != ':') { + continue; + } + + String nameString = new String(buf, mark, pos - mark - 1, StandardCharsets.US_ASCII); + + if (buf[pos++] != ' ') { + throw new IOException(String.format("Invalid value for attribute '%s'", nameString)); + } + + try { + name = attributeNameCache.get(nameString); + if (name == null) { + name = new Attributes.Name(nameString); + attributeNameCache.put(nameString, name); + } + } catch (IllegalArgumentException e) { + // new Attributes.Name() throws IllegalArgumentException but we declare IOException + throw new IOException(e.getMessage()); + } + return; + } + } + + private void readValue() throws IOException { + boolean lastCr = false; + int mark = pos; + int last = pos; + valueBuffer.reset(); + while (pos < buf.length) { + byte next = buf[pos++]; + switch (next) { + case 0: + throw new IOException("NUL character in a manifest"); + case '\n': + if (lastCr) { + lastCr = false; + } else { + consecutiveLineBreaks++; + } + continue; + case '\r': + lastCr = true; + consecutiveLineBreaks++; + continue; + case ' ': + if (consecutiveLineBreaks == 1) { + valueBuffer.write(buf, mark, last - mark); + mark = pos; + consecutiveLineBreaks = 0; + continue; + } + } + + if (consecutiveLineBreaks >= 1) { + pos--; + break; + } + last = pos; + } + + valueBuffer.write(buf, mark, last - mark); + // A bit frustrating that that Charset.forName will be called + // again. + value = valueBuffer.toString(StandardCharsets.UTF_8.name()); + } +} diff --git a/core/java/android/util/jar/StrictJarVerifier.java b/core/java/android/util/jar/StrictJarVerifier.java new file mode 100644 index 000000000000..ca2aec105bcf --- /dev/null +++ b/core/java/android/util/jar/StrictJarVerifier.java @@ -0,0 +1,456 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util.jar; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import libcore.io.Base64; +import sun.security.jca.Providers; +import sun.security.pkcs.PKCS7; + +/** + * Non-public class used by {@link JarFile} and {@link JarInputStream} to manage + * the verification of signed JARs. {@code JarFile} and {@code JarInputStream} + * objects are expected to have a {@code JarVerifier} instance member which + * can be used to carry out the tasks associated with verifying a signed JAR. + * These tasks would typically include: + *

    + *
  • verification of all signed signature files + *
  • confirmation that all signed data was signed only by the party or parties + * specified in the signature block data + *
  • verification that the contents of all signature files (i.e. {@code .SF} + * files) agree with the JAR entries information found in the JAR manifest. + *
+ */ +class StrictJarVerifier { + /** + * List of accepted digest algorithms. This list is in order from most + * preferred to least preferred. + */ + private static final String[] DIGEST_ALGORITHMS = new String[] { + "SHA-512", + "SHA-384", + "SHA-256", + "SHA1", + }; + + private final String jarName; + private final StrictJarManifest manifest; + private final HashMap metaEntries; + private final int mainAttributesEnd; + + private final Hashtable> signatures = + new Hashtable>(5); + + private final Hashtable certificates = + new Hashtable(5); + + private final Hashtable verifiedEntries = + new Hashtable(); + + /** + * Stores and a hash and a message digest and verifies that massage digest + * matches the hash. + */ + static class VerifierEntry extends OutputStream { + + private final String name; + + private final MessageDigest digest; + + private final byte[] hash; + + private final Certificate[][] certChains; + + private final Hashtable verifiedEntries; + + VerifierEntry(String name, MessageDigest digest, byte[] hash, + Certificate[][] certChains, Hashtable verifedEntries) { + this.name = name; + this.digest = digest; + this.hash = hash; + this.certChains = certChains; + this.verifiedEntries = verifedEntries; + } + + /** + * Updates a digest with one byte. + */ + @Override + public void write(int value) { + digest.update((byte) value); + } + + /** + * Updates a digest with byte array. + */ + @Override + public void write(byte[] buf, int off, int nbytes) { + digest.update(buf, off, nbytes); + } + + /** + * Verifies that the digests stored in the manifest match the decrypted + * digests from the .SF file. This indicates the validity of the + * signing, not the integrity of the file, as its digest must be + * calculated and verified when its contents are read. + * + * @throws SecurityException + * if the digest value stored in the manifest does not + * agree with the decrypted digest as recovered from the + * .SF file. + */ + void verify() { + byte[] d = digest.digest(); + if (!MessageDigest.isEqual(d, Base64.decode(hash))) { + throw invalidDigest(JarFile.MANIFEST_NAME, name, name); + } + verifiedEntries.put(name, certChains); + } + } + + private static SecurityException invalidDigest(String signatureFile, String name, + String jarName) { + throw new SecurityException(signatureFile + " has invalid digest for " + name + + " in " + jarName); + } + + private static SecurityException failedVerification(String jarName, String signatureFile) { + throw new SecurityException(jarName + " failed verification of " + signatureFile); + } + + private static SecurityException failedVerification(String jarName, String signatureFile, + Throwable e) { + throw new SecurityException(jarName + " failed verification of " + signatureFile, e); + } + + + /** + * Constructs and returns a new instance of {@code JarVerifier}. + * + * @param name + * the name of the JAR file being verified. + */ + StrictJarVerifier(String name, StrictJarManifest manifest, + HashMap metaEntries) { + jarName = name; + this.manifest = manifest; + this.metaEntries = metaEntries; + this.mainAttributesEnd = manifest.getMainAttributesEnd(); + } + + /** + * Invoked for each new JAR entry read operation from the input + * stream. This method constructs and returns a new {@link VerifierEntry} + * which contains the certificates used to sign the entry and its hash value + * as specified in the JAR MANIFEST format. + * + * @param name + * the name of an entry in a JAR file which is not in the + * {@code META-INF} directory. + * @return a new instance of {@link VerifierEntry} which can be used by + * callers as an {@link OutputStream}. + */ + VerifierEntry initEntry(String name) { + // If no manifest is present by the time an entry is found, + // verification cannot occur. If no signature files have + // been found, do not verify. + if (manifest == null || signatures.isEmpty()) { + return null; + } + + Attributes attributes = manifest.getAttributes(name); + // entry has no digest + if (attributes == null) { + return null; + } + + ArrayList certChains = new ArrayList(); + Iterator>> it = signatures.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + HashMap hm = entry.getValue(); + if (hm.get(name) != null) { + // Found an entry for entry name in .SF file + String signatureFile = entry.getKey(); + Certificate[] certChain = certificates.get(signatureFile); + if (certChain != null) { + certChains.add(certChain); + } + } + } + + // entry is not signed + if (certChains.isEmpty()) { + return null; + } + Certificate[][] certChainsArray = certChains.toArray(new Certificate[certChains.size()][]); + + for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) { + final String algorithm = DIGEST_ALGORITHMS[i]; + final String hash = attributes.getValue(algorithm + "-Digest"); + if (hash == null) { + continue; + } + byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1); + + try { + return new VerifierEntry(name, MessageDigest.getInstance(algorithm), hashBytes, + certChainsArray, verifiedEntries); + } catch (NoSuchAlgorithmException ignored) { + } + } + return null; + } + + /** + * Add a new meta entry to the internal collection of data held on each JAR + * entry in the {@code META-INF} directory including the manifest + * file itself. Files associated with the signing of a JAR would also be + * added to this collection. + * + * @param name + * the name of the file located in the {@code META-INF} + * directory. + * @param buf + * the file bytes for the file called {@code name}. + * @see #removeMetaEntries() + */ + void addMetaEntry(String name, byte[] buf) { + metaEntries.put(name.toUpperCase(Locale.US), buf); + } + + /** + * If the associated JAR file is signed, check on the validity of all of the + * known signatures. + * + * @return {@code true} if the associated JAR is signed and an internal + * check verifies the validity of the signature(s). {@code false} if + * the associated JAR file has no entries at all in its {@code + * META-INF} directory. This situation is indicative of an invalid + * JAR file. + *

+ * Will also return {@code true} if the JAR file is not + * signed. + * @throws SecurityException + * if the JAR file is signed and it is determined that a + * signature block file contains an invalid signature for the + * corresponding signature file. + */ + synchronized boolean readCertificates() { + if (metaEntries.isEmpty()) { + return false; + } + + Iterator it = metaEntries.keySet().iterator(); + while (it.hasNext()) { + String key = it.next(); + if (key.endsWith(".DSA") || key.endsWith(".RSA") || key.endsWith(".EC")) { + verifyCertificate(key); + it.remove(); + } + } + return true; + } + + /** + * Verifies that the signature computed from {@code sfBytes} matches + * that specified in {@code blockBytes} (which is a PKCS7 block). Returns + * certificates listed in the PKCS7 block. Throws a {@code GeneralSecurityException} + * if something goes wrong during verification. + */ + static Certificate[] verifyBytes(byte[] blockBytes, byte[] sfBytes) + throws GeneralSecurityException { + + Object obj = null; + try { + + obj = Providers.startJarVerification(); + PKCS7 block = new PKCS7(blockBytes); + if (block.verify(sfBytes) == null) { + throw new GeneralSecurityException("Failed to verify signature"); + } + X509Certificate[] blockCerts = block.getCertificates(); + Certificate[] signerCertChain = null; + if (blockCerts != null) { + signerCertChain = new Certificate[blockCerts.length]; + for (int i = 0; i < blockCerts.length; ++i) { + signerCertChain[i] = blockCerts[i]; + } + } + return signerCertChain; + } catch (IOException e) { + throw new GeneralSecurityException("IO exception verifying jar cert", e); + } finally { + Providers.stopJarVerification(obj); + } + } + + /** + * @param certFile + */ + private void verifyCertificate(String certFile) { + // Found Digital Sig, .SF should already have been read + String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) + ".SF"; + byte[] sfBytes = metaEntries.get(signatureFile); + if (sfBytes == null) { + return; + } + + byte[] manifestBytes = metaEntries.get(JarFile.MANIFEST_NAME); + // Manifest entry is required for any verifications. + if (manifestBytes == null) { + return; + } + + byte[] sBlockBytes = metaEntries.get(certFile); + try { + Certificate[] signerCertChain = verifyBytes(sBlockBytes, sfBytes); + if (signerCertChain != null) { + certificates.put(signatureFile, signerCertChain); + } + } catch (GeneralSecurityException e) { + throw failedVerification(jarName, signatureFile, e); + } + + // Verify manifest hash in .sf file + Attributes attributes = new Attributes(); + HashMap entries = new HashMap(); + try { + StrictJarManifestReader im = new StrictJarManifestReader(sfBytes, attributes); + im.readEntries(entries, null); + } catch (IOException e) { + return; + } + + // Do we actually have any signatures to look at? + if (attributes.get(Attributes.Name.SIGNATURE_VERSION) == null) { + return; + } + + boolean createdBySigntool = false; + String createdBy = attributes.getValue("Created-By"); + if (createdBy != null) { + createdBySigntool = createdBy.indexOf("signtool") != -1; + } + + // Use .SF to verify the mainAttributes of the manifest + // If there is no -Digest-Manifest-Main-Attributes entry in .SF + // file, such as those created before java 1.5, then we ignore + // such verification. + if (mainAttributesEnd > 0 && !createdBySigntool) { + String digestAttribute = "-Digest-Manifest-Main-Attributes"; + if (!verify(attributes, digestAttribute, manifestBytes, 0, mainAttributesEnd, false, true)) { + throw failedVerification(jarName, signatureFile); + } + } + + // Use .SF to verify the whole manifest. + String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest"; + if (!verify(attributes, digestAttribute, manifestBytes, 0, manifestBytes.length, false, false)) { + Iterator> it = entries.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + StrictJarManifest.Chunk chunk = manifest.getChunk(entry.getKey()); + if (chunk == null) { + return; + } + if (!verify(entry.getValue(), "-Digest", manifestBytes, + chunk.start, chunk.end, createdBySigntool, false)) { + throw invalidDigest(signatureFile, entry.getKey(), jarName); + } + } + } + metaEntries.put(signatureFile, null); + signatures.put(signatureFile, entries); + } + + /** + * Returns a boolean indication of whether or not the + * associated jar file is signed. + * + * @return {@code true} if the JAR is signed, {@code false} + * otherwise. + */ + boolean isSignedJar() { + return certificates.size() > 0; + } + + private boolean verify(Attributes attributes, String entry, byte[] data, + int start, int end, boolean ignoreSecondEndline, boolean ignorable) { + for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) { + String algorithm = DIGEST_ALGORITHMS[i]; + String hash = attributes.getValue(algorithm + entry); + if (hash == null) { + continue; + } + + MessageDigest md; + try { + md = MessageDigest.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + continue; + } + if (ignoreSecondEndline && data[end - 1] == '\n' && data[end - 2] == '\n') { + md.update(data, start, end - 1 - start); + } else { + md.update(data, start, end - start); + } + byte[] b = md.digest(); + byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1); + return MessageDigest.isEqual(b, Base64.decode(hashBytes)); + } + return ignorable; + } + + /** + * Returns all of the {@link java.security.cert.Certificate} chains that + * were used to verify the signature on the JAR entry called + * {@code name}. Callers must not modify the returned arrays. + * + * @param name + * the name of a JAR entry. + * @return an array of {@link java.security.cert.Certificate} chains. + */ + Certificate[][] getCertificateChains(String name) { + return verifiedEntries.get(name); + } + + /** + * Remove all entries from the internal collection of data held about each + * JAR entry in the {@code META-INF} directory. + */ + void removeMetaEntries() { + metaEntries.clear(); + } +} diff --git a/core/jni/Android.mk b/core/jni/Android.mk index 86412926b3ce..d9ede93467c4 100644 --- a/core/jni/Android.mk +++ b/core/jni/Android.mk @@ -93,6 +93,7 @@ LOCAL_SRC_FILES:= \ android_util_Process.cpp \ android_util_StringBlock.cpp \ android_util_XmlBlock.cpp \ + android_util_jar_StrictJarFile.cpp \ android_graphics_Canvas.cpp \ android_graphics_Picture.cpp \ android/graphics/AutoDecodeCancel.cpp \ diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index b8aa2f0eaaeb..6a71d5fc9fcf 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -176,6 +176,7 @@ extern int register_android_app_backup_FullBackup(JNIEnv *env); extern int register_android_app_ActivityThread(JNIEnv *env); extern int register_android_app_NativeActivity(JNIEnv *env); extern int register_android_media_RemoteDisplay(JNIEnv *env); +extern int register_android_util_jar_StrictJarFile(JNIEnv* env); extern int register_android_view_InputChannel(JNIEnv* env); extern int register_android_view_InputDevice(JNIEnv* env); extern int register_android_view_InputEventReceiver(JNIEnv* env); @@ -1359,6 +1360,7 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_app_backup_FullBackup), REG_JNI(register_android_app_ActivityThread), REG_JNI(register_android_app_NativeActivity), + REG_JNI(register_android_util_jar_StrictJarFile), REG_JNI(register_android_view_InputChannel), REG_JNI(register_android_view_InputEventReceiver), REG_JNI(register_android_view_InputEventSender), @@ -1374,6 +1376,8 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_animation_PropertyValuesHolder), REG_JNI(register_com_android_internal_content_NativeLibraryHelper), REG_JNI(register_com_android_internal_net_NetworkStatsFactory), + + }; /* diff --git a/core/jni/android_util_jar_StrictJarFile.cpp b/core/jni/android_util_jar_StrictJarFile.cpp new file mode 100644 index 000000000000..7f8f70832ab7 --- /dev/null +++ b/core/jni/android_util_jar_StrictJarFile.cpp @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "StrictJarFile" + +#include +#include + +#include "JNIHelp.h" +#include "JniConstants.h" +#include "ScopedLocalRef.h" +#include "ScopedUtfChars.h" +#include "jni.h" +#include "ziparchive/zip_archive.h" +#include "cutils/log.h" + +namespace android { + +// The method ID for ZipEntry.(String,String,JJJIII[BJJ) +static jmethodID zipEntryCtor; + +static void throwIoException(JNIEnv* env, const int32_t errorCode) { + jniThrowException(env, "java/io/IOException", ErrorCodeString(errorCode)); +} + +static jobject newZipEntry(JNIEnv* env, const ZipEntry& entry, jstring entryName) { + return env->NewObject(JniConstants::zipEntryClass, + zipEntryCtor, + entryName, + NULL, // comment + static_cast(entry.crc32), + static_cast(entry.compressed_length), + static_cast(entry.uncompressed_length), + static_cast(entry.method), + static_cast(0), // time + NULL, // byte[] extra + static_cast(entry.offset)); +} + +static jlong StrictJarFile_nativeOpenJarFile(JNIEnv* env, jobject, jstring fileName) { + ScopedUtfChars fileChars(env, fileName); + if (fileChars.c_str() == NULL) { + return static_cast(-1); + } + + ZipArchiveHandle handle; + int32_t error = OpenArchive(fileChars.c_str(), &handle); + if (error) { + CloseArchive(handle); + throwIoException(env, error); + return static_cast(-1); + } + + return reinterpret_cast(handle); +} + +class IterationHandle { + public: + IterationHandle() : + cookie_(NULL) { + } + + void** CookieAddress() { + return &cookie_; + } + + ~IterationHandle() { + EndIteration(cookie_); + } + + private: + void* cookie_; +}; + + +static jlong StrictJarFile_nativeStartIteration(JNIEnv* env, jobject, jlong nativeHandle, + jstring prefix) { + ScopedUtfChars prefixChars(env, prefix); + if (prefixChars.c_str() == NULL) { + return static_cast(-1); + } + + IterationHandle* handle = new IterationHandle(); + int32_t error = 0; + if (prefixChars.size() == 0) { + error = StartIteration(reinterpret_cast(nativeHandle), + handle->CookieAddress(), NULL, NULL); + } else { + ZipString entry_name(prefixChars.c_str()); + error = StartIteration(reinterpret_cast(nativeHandle), + handle->CookieAddress(), &entry_name, NULL); + } + + if (error) { + throwIoException(env, error); + return static_cast(-1); + } + + return reinterpret_cast(handle); +} + +static jobject StrictJarFile_nativeNextEntry(JNIEnv* env, jobject, jlong iterationHandle) { + ZipEntry data; + ZipString entryName; + + IterationHandle* handle = reinterpret_cast(iterationHandle); + const int32_t error = Next(*handle->CookieAddress(), &data, &entryName); + if (error) { + delete handle; + return NULL; + } + + std::unique_ptr entryNameCString(new char[entryName.name_length + 1]); + memcpy(entryNameCString.get(), entryName.name, entryName.name_length); + entryNameCString[entryName.name_length] = '\0'; + ScopedLocalRef entryNameString(env, env->NewStringUTF(entryNameCString.get())); + + return newZipEntry(env, data, entryNameString.get()); +} + +static jobject StrictJarFile_nativeFindEntry(JNIEnv* env, jobject, jlong nativeHandle, + jstring entryName) { + ScopedUtfChars entryNameChars(env, entryName); + if (entryNameChars.c_str() == NULL) { + return NULL; + } + + ZipEntry data; + const int32_t error = FindEntry(reinterpret_cast(nativeHandle), + ZipString(entryNameChars.c_str()), &data); + if (error) { + return NULL; + } + + return newZipEntry(env, data, entryName); +} + +static void StrictJarFile_nativeClose(JNIEnv*, jobject, jlong nativeHandle) { + CloseArchive(reinterpret_cast(nativeHandle)); +} + +static JNINativeMethod gMethods[] = { + NATIVE_METHOD(StrictJarFile, nativeOpenJarFile, "(Ljava/lang/String;)J"), + NATIVE_METHOD(StrictJarFile, nativeStartIteration, "(JLjava/lang/String;)J"), + NATIVE_METHOD(StrictJarFile, nativeNextEntry, "(J)Ljava/util/zip/ZipEntry;"), + NATIVE_METHOD(StrictJarFile, nativeFindEntry, "(JLjava/lang/String;)Ljava/util/zip/ZipEntry;"), + NATIVE_METHOD(StrictJarFile, nativeClose, "(J)V"), +}; + +void register_android_util_jar_StrictJarFile(JNIEnv* env) { + jniRegisterNativeMethods(env, "android/util/jar/StrictJarFile", gMethods, NELEM(gMethods)); + + zipEntryCtor = env->GetMethodID(JniConstants::zipEntryClass, "", + "(Ljava/lang/String;Ljava/lang/String;JJJII[BJ)V"); + LOG_ALWAYS_FATAL_IF(zipEntryCtor == NULL, "Unable to find ZipEntry."); +} + +}; // namespace android diff --git a/preloaded-classes b/preloaded-classes index be1f1c52ad22..2d02932b2471 100644 --- a/preloaded-classes +++ b/preloaded-classes @@ -1765,6 +1765,7 @@ android.util.StateSet android.util.SuperNotCalledException android.util.TypedValue android.util.Xml +android.util.jar.StrictJarFile android.view.AbsSavedState android.view.AbsSavedState$1 android.view.AbsSavedState$2 @@ -3361,9 +3362,6 @@ java.util.jar.Attributes$Name java.util.jar.JarEntry java.util.jar.JarFile java.util.jar.JarFile$JarFileEnumerator -java.util.jar.Manifest -java.util.jar.ManifestReader -java.util.jar.StrictJarFile java.util.logging.ConsoleHandler java.util.logging.ErrorManager java.util.logging.Filter @@ -3824,4 +3822,11 @@ org.xml.sax.helpers.DefaultHandler org.xmlpull.v1.XmlPullParser org.xmlpull.v1.XmlPullParserException org.xmlpull.v1.XmlSerializer +<<<<<<< HEAD sun.misc.Unsafe +======= +<<<<<<< HEAD +======= +sun.misc.Unsafe +>>>>>>> 631d21f... Move StrictJarFile from libcore to framework +>>>>>>> 43ea2cc... DO NOT MERGE Move StrictJarFile from libcore to framework -- 2.11.0