Repository: commons-compress
Updated Branches:
  refs/heads/master 53f6d42f7 -> 22b0d0629


COMPRESS-455 handle APK Signing Block


Project: http://git-wip-us.apache.org/repos/asf/commons-compress/repo
Commit: http://git-wip-us.apache.org/repos/asf/commons-compress/commit/af6fe141
Tree: http://git-wip-us.apache.org/repos/asf/commons-compress/tree/af6fe141
Diff: http://git-wip-us.apache.org/repos/asf/commons-compress/diff/af6fe141

Branch: refs/heads/master
Commit: af6fe141036d30bfd1613758b7a9fb413bf2bafc
Parents: 53f6d42
Author: Stefan Bodewig <bode...@apache.org>
Authored: Sun Jul 1 13:45:56 2018 +0200
Committer: Stefan Bodewig <bode...@apache.org>
Committed: Sun Jul 1 13:45:56 2018 +0200

----------------------------------------------------------------------
 src/changes/changes.xml                         |  5 ++
 .../archivers/zip/ZipArchiveInputStream.java    | 71 ++++++++++++++++++--
 2 files changed, 72 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/commons-compress/blob/af6fe141/src/changes/changes.xml
----------------------------------------------------------------------
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 @@ The <action> type attribute can be add,update,fix,remove.
         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">

http://git-wip-us.apache.org/repos/asf/commons-compress/blob/af6fe141/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java
----------------------------------------------------------------------
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.EOFException;
 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 class ZipArchiveInputStream extends 
ArchiveInputStream implements InputSt
         }
 
         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 @@ public class ZipArchiveInputStream extends 
ArchiveInputStream implements InputSt
     }
 
     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 @@ public class ZipArchiveInputStream extends 
ArchiveInputStream implements InputSt
         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.

Reply via email to