COMPRESS-455 handle APK Signing Block
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 5a620cb..82cf229 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -59,6 +59,11 @@
ensure all held resources get closed even if exceptions are
thrown during the closing the stream.
</action>
+ <action issue="COMPRESS-455" type="fix" date="2018-07-01">
+ ZipArchiveInputStream can now detect the APK Signing Block
+ used in signed Android APK files and treats it as an "end of
+ archive" marker.
+ </action>
</release>
<release version="1.17" date="2018-06-03"
description="Release 1.17">
diff --git a/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java b/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java
index 729d92e..77d6d70 100644
--- a/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java
+++ b/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java
@@ -24,7 +24,9 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
+import java.math.BigInteger;
import java.nio.ByteBuffer;
+import java.util.Arrays;
import java.util.zip.CRC32;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
@@ -249,12 +251,12 @@ public ZipArchiveEntry getNextZipEntry() throws IOException {
}
final ZipLong sig = new ZipLong(lfhBuf);
- if (sig.equals(ZipLong.CFH_SIG) || sig.equals(ZipLong.AED_SIG)) {
+ if (!sig.equals(ZipLong.LFH_SIG)) {
+ if (sig.equals(ZipLong.CFH_SIG) || sig.equals(ZipLong.AED_SIG) || isApkSigningBlock(lfhBuf)) {
hitCentralDirectory = true;
skipRemainderOfArchive();
return null;
}
- if (!sig.equals(ZipLong.LFH_SIG)) {
throw new ZipException(String.format("Unexpected record signature: 0X%X", sig.getValue()));
}
@@ -789,9 +791,14 @@ private int fill() throws IOException {
}
private void readFully(final byte[] b) throws IOException {
- final int count = IOUtils.readFully(in, b);
+ readFully(b, 0);
+ }
+
+ private void readFully(final byte[] b, final int off) throws IOException {
+ final int len = b.length - off;
+ final int count = IOUtils.readFully(in, b, off, len);
count(count);
- if (count < b.length) {
+ if (count < len) {
throw new EOFException();
}
}
@@ -1087,6 +1094,62 @@ private boolean isFirstByteOfEocdSig(final int b) {
return b == ZipArchiveOutputStream.EOCD_SIG[0];
}
+ private static final byte[] APK_SIGNING_BLOCK_MAGIC = new byte[] {
+ 'A', 'P', 'K', ' ', 'S', 'i', 'g', ' ', 'B', 'l', 'o', 'c', 'k', ' ', '4', '2',
+ };
+ private static final BigInteger LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE);
+
+ /**
+ * Checks whether this might be an APK Signing Block.
+ *
+ * <p>Unfortunately the APK signing block does not start with some kind of signature, it rather ends with one. It
+ * starts with a length, so what we do is parse the suspect length, skip ahead far enough, look for the signature
+ * and if we've found it, return true.</p>
+ *
+ * @param suspectLocalFileHeader the bytes read from the underlying stream in the expectation that they would hold
+ * the local file header of the next entry.
+ *
+ * @return true if this looks like a APK signing block
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2">https://source.android.com/security/apksigning/v2</a>
+ */
+ private boolean isApkSigningBlock(byte[] suspectLocalFileHeader) throws IOException {
+ // length of block excluding the size field itself
+ BigInteger len = ZipEightByteInteger.getValue(suspectLocalFileHeader);
+ // LFH has already been read and all but the first eight bytes contain (part of) the APK signing block,
+ // also subtract 16 bytes in order to position us at the magic string
+ BigInteger toSkip = len.add(BigInteger.valueOf(DWORD - suspectLocalFileHeader.length
+ - APK_SIGNING_BLOCK_MAGIC.length));
+ byte[] magic = new byte[APK_SIGNING_BLOCK_MAGIC.length];
+
+ try {
+ if (toSkip.signum() < 0) {
+ // suspectLocalFileHeader contains the start of suspect magic string
+ int off = suspectLocalFileHeader.length + toSkip.intValue();
+ // length was shorter than magic length
+ if (off < DWORD) {
+ return false;
+ }
+ int bytesInBuffer = Math.abs(toSkip.intValue());
+ System.arraycopy(suspectLocalFileHeader, off, magic, 0, Math.min(bytesInBuffer, magic.length));
+ if (bytesInBuffer < magic.length) {
+ readFully(magic, bytesInBuffer);
+ }
+ } else {
+ while (toSkip.compareTo(LONG_MAX) > 0) {
+ realSkip(Long.MAX_VALUE);
+ toSkip = toSkip.add(LONG_MAX.negate());
+ }
+ realSkip(toSkip.longValue());
+ readFully(magic);
+ }
+ } catch (EOFException ex) {
+ // length was invalid
+ return false;
+ }
+ return Arrays.equals(magic, APK_SIGNING_BLOCK_MAGIC);
+ }
+
/**
* Structure collecting information for the entry that is
* currently being read.