Merge "Move StrictJarFile from libcore to framework"
This commit is contained in:
committed by
Android (Google) Code Review
commit
ef3dc06bfb
@@ -84,9 +84,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
|
||||
|
||||
427
core/java/android/util/jar/StrictJarFile.java
Normal file
427
core/java/android/util/jar/StrictJarFile.java
Normal file
@@ -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<String, byte[]> metaEntries = getMetaEntries();
|
||||
this.manifest = new StrictJarManifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
|
||||
this.verifier = new StrictJarVerifier(fileName, manifest, metaEntries);
|
||||
Set<String> 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<ZipEntry> 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<ZipEntry> {
|
||||
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<String, byte[]> getMetaEntries() throws IOException {
|
||||
HashMap<String, byte[]> metaEntries = new HashMap<String, byte[]>();
|
||||
|
||||
Iterator<ZipEntry> 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.)
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
315
core/java/android/util/jar/StrictJarManifest.java
Normal file
315
core/java/android/util/jar/StrictJarManifest.java
Normal file
@@ -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<String, Attributes> entries;
|
||||
|
||||
static final class Chunk {
|
||||
final int start;
|
||||
final int end;
|
||||
|
||||
Chunk(int start, int end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
}
|
||||
|
||||
private HashMap<String, Chunk> 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<String, Attributes>();
|
||||
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<String, Attributes>) ((HashMap<String, Attributes>) man
|
||||
.getEntries()).clone();
|
||||
}
|
||||
|
||||
StrictJarManifest(byte[] manifestBytes, boolean readChunks) throws IOException {
|
||||
this();
|
||||
if (readChunks) {
|
||||
chunks = new HashMap<String, Chunk>();
|
||||
}
|
||||
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<String, Attributes> 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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
184
core/java/android/util/jar/StrictJarManifestReader.java
Normal file
184
core/java/android/util/jar/StrictJarManifestReader.java
Normal file
@@ -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<String, Attributes.Name> attributeNameCache = new HashMap<String, Attributes.Name>();
|
||||
|
||||
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<String, Attributes> entries, Map<String, StrictJarManifest.Chunk> 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());
|
||||
}
|
||||
}
|
||||
456
core/java/android/util/jar/StrictJarVerifier.java
Normal file
456
core/java/android/util/jar/StrictJarVerifier.java
Normal file
@@ -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:
|
||||
* <ul>
|
||||
* <li>verification of all signed signature files
|
||||
* <li>confirmation that all signed data was signed only by the party or parties
|
||||
* specified in the signature block data
|
||||
* <li>verification that the contents of all signature files (i.e. {@code .SF}
|
||||
* files) agree with the JAR entries information found in the JAR manifest.
|
||||
* </ul>
|
||||
*/
|
||||
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<String, byte[]> metaEntries;
|
||||
private final int mainAttributesEnd;
|
||||
|
||||
private final Hashtable<String, HashMap<String, Attributes>> signatures =
|
||||
new Hashtable<String, HashMap<String, Attributes>>(5);
|
||||
|
||||
private final Hashtable<String, Certificate[]> certificates =
|
||||
new Hashtable<String, Certificate[]>(5);
|
||||
|
||||
private final Hashtable<String, Certificate[][]> verifiedEntries =
|
||||
new Hashtable<String, Certificate[][]>();
|
||||
|
||||
/**
|
||||
* 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<String, Certificate[][]> verifiedEntries;
|
||||
|
||||
VerifierEntry(String name, MessageDigest digest, byte[] hash,
|
||||
Certificate[][] certChains, Hashtable<String, Certificate[][]> 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 <i>not</i>
|
||||
* agree with the decrypted digest as recovered from the
|
||||
* <code>.SF</code> 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<String, byte[]> 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 <b>not</b> 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<Certificate[]> certChains = new ArrayList<Certificate[]>();
|
||||
Iterator<Map.Entry<String, HashMap<String, Attributes>>> it = signatures.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<String, HashMap<String, Attributes>> entry = it.next();
|
||||
HashMap<String, Attributes> 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.
|
||||
* <p>
|
||||
* Will also return {@code true} if the JAR file is <i>not</i>
|
||||
* 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<String> 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<String, Attributes> entries = new HashMap<String, Attributes>();
|
||||
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<Map.Entry<String, Attributes>> it = entries.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<String, Attributes> 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 <code>boolean</code> 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();
|
||||
}
|
||||
}
|
||||
@@ -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 \
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
172
core/jni/android_util_jar_StrictJarFile.cpp
Normal file
172
core/jni/android_util_jar_StrictJarFile.cpp
Normal file
@@ -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 <memory>
|
||||
#include <string>
|
||||
|
||||
#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.<init>(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<jlong>(entry.crc32),
|
||||
static_cast<jlong>(entry.compressed_length),
|
||||
static_cast<jlong>(entry.uncompressed_length),
|
||||
static_cast<jint>(entry.method),
|
||||
static_cast<jint>(0), // time
|
||||
NULL, // byte[] extra
|
||||
static_cast<jlong>(entry.offset));
|
||||
}
|
||||
|
||||
static jlong StrictJarFile_nativeOpenJarFile(JNIEnv* env, jobject, jstring fileName) {
|
||||
ScopedUtfChars fileChars(env, fileName);
|
||||
if (fileChars.c_str() == NULL) {
|
||||
return static_cast<jlong>(-1);
|
||||
}
|
||||
|
||||
ZipArchiveHandle handle;
|
||||
int32_t error = OpenArchive(fileChars.c_str(), &handle);
|
||||
if (error) {
|
||||
CloseArchive(handle);
|
||||
throwIoException(env, error);
|
||||
return static_cast<jlong>(-1);
|
||||
}
|
||||
|
||||
return reinterpret_cast<jlong>(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<jlong>(-1);
|
||||
}
|
||||
|
||||
IterationHandle* handle = new IterationHandle();
|
||||
int32_t error = 0;
|
||||
if (prefixChars.size() == 0) {
|
||||
error = StartIteration(reinterpret_cast<ZipArchiveHandle>(nativeHandle),
|
||||
handle->CookieAddress(), NULL, NULL);
|
||||
} else {
|
||||
ZipString entry_name(prefixChars.c_str());
|
||||
error = StartIteration(reinterpret_cast<ZipArchiveHandle>(nativeHandle),
|
||||
handle->CookieAddress(), &entry_name, NULL);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throwIoException(env, error);
|
||||
return static_cast<jlong>(-1);
|
||||
}
|
||||
|
||||
return reinterpret_cast<jlong>(handle);
|
||||
}
|
||||
|
||||
static jobject StrictJarFile_nativeNextEntry(JNIEnv* env, jobject, jlong iterationHandle) {
|
||||
ZipEntry data;
|
||||
ZipString entryName;
|
||||
|
||||
IterationHandle* handle = reinterpret_cast<IterationHandle*>(iterationHandle);
|
||||
const int32_t error = Next(*handle->CookieAddress(), &data, &entryName);
|
||||
if (error) {
|
||||
delete handle;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
std::unique_ptr<char[]> entryNameCString(new char[entryName.name_length + 1]);
|
||||
memcpy(entryNameCString.get(), entryName.name, entryName.name_length);
|
||||
entryNameCString[entryName.name_length] = '\0';
|
||||
ScopedLocalRef<jstring> 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<ZipArchiveHandle>(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<ZipArchiveHandle>(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, "<init>",
|
||||
"(Ljava/lang/String;Ljava/lang/String;JJJII[BJ)V");
|
||||
LOG_ALWAYS_FATAL_IF(zipEntryCtor == NULL, "Unable to find ZipEntry.<init>");
|
||||
}
|
||||
|
||||
}; // namespace android
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user