├── .gitignore ├── .idea ├── .gitignore ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml ├── uiDesigner.xml └── vcs.xml ├── ApkDataMultiplexing.iml ├── README.MD ├── src └── bin │ ├── io │ ├── BufferedRandomAccessFile.java │ ├── FragmentRandomAccessData.java │ ├── RandomAccessData.java │ ├── RandomAccessDataImpl.java │ ├── RandomAccessFactory.java │ └── RandomAccessFile.java │ ├── mt │ └── apksign │ │ ├── ByteArrayUtil.java │ │ ├── SignatureAlgorithm.java │ │ ├── V2V3SchemeSigner.java │ │ ├── VerityTreeBuilder.java │ │ ├── ZipBuffer.java │ │ ├── data │ │ ├── ByteArrayDataSink.java │ │ ├── ByteArrayDataSource.java │ │ ├── ChainedDataSource.java │ │ ├── DataSink.java │ │ ├── DataSinks.java │ │ ├── DataSource.java │ │ ├── DataSources.java │ │ └── FileDataSource.java │ │ └── key │ │ ├── JksSignatureKey.java │ │ └── SignatureKey.java │ └── zip │ ├── BridgeInputStream.java │ ├── BridgeOutputStream.java │ ├── CenterFileHeader.java │ ├── CrcOutputStream.java │ ├── DataMultiplexing.java │ ├── ExtraDataRecord.java │ ├── NoWrapDeflaterOutputStream.java │ ├── NoWrapInflaterInputStream.java │ ├── ZipConstant.java │ ├── ZipEntry.java │ ├── ZipFile.java │ ├── ZipMaker.java │ └── ZipUtil.java ├── test.apk └── test.jks /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | output.apk -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ApkDataMultiplexing.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # APK数据复用优化 2 | 3 | 如今越来越多的 APP 采用了 apk 文件完整性校验来检测是否被修改,原先 hook 签名数据的方式已无法通过该检测,对于该情况需要使用原包过签方式。 4 | 5 | 其原理是将原 apk 文件打包到过签包中,运行时将原 apk 文件释放到本地并进行 IO 重定向,实现对抗整包校验,但这也导致了过签包的体积是原体积的两倍以上。 6 | 7 | ## 过签包结构 8 | 9 | - 过签包 10 | - assets/base.apk (原包) 11 | - AndroidManifest.xml 12 | - classes.dex 13 | - resources.arsc 14 | - res/a 15 | - res/b 16 | - lib/arm64-v8a/libxxx.so(增) 17 | - AndroidManifest.xml(改) 18 | - classes.dex(改) 19 | - resources.arsc(改) 20 | - res/a 21 | - res/b 22 | 23 | 以上是某个过签包的文件结构,assets/base.apk 是原包,其体积占了整个过签包体积的近一半,如果我们对比这个过签包与原包的内容,会发现过签包 24 | 25 | 新增了: 26 | 27 | - assets/base.apk 28 | - lib/arm64-v8a/libxxx.so 29 | 30 | 修改了: 31 | 32 | - AndroidManifest.xml 33 | - classes.dex 34 | - resources.arsc 35 | 36 | 以下文件则完全一样: 37 | 38 | - res/a 39 | - res/b 40 | 41 | 如果这 2 个一样的文件有 10 MB,那么它们会在过签包中占据 20 MB 的空间,造成了空间浪费,而这个是可以优化的。 42 | 43 | ## 体积优化思路 44 | 45 | 我们知道 APK 文件使用的是 ZIP 格式,而 ZIP 文件末尾有一个中央目录记录了所有的文件头和它们的数据偏移。 46 | 47 | 经过测试,安卓系统读取 apk 文件时,先读取中央目录来获取所有文件信息,再根据数据偏移读取文件数据。 48 | 49 | 重点来了,文件数据是通过中央目录的偏移去定位的,而过签包和原包都是 ZIP 结构,那么对于相同的文件,我们可以让过签包和原包的中央目录数据偏移都指向同一个地址,删除另一个地址的数据段,于是就实现了体积优化。 50 | 51 | 具体如下: 52 | 53 | **优化前** 54 | 55 | - 过签包 56 | - assets/base.apk 57 | - 数据段1 res/a 58 | - 数据段2 res/b 59 | - 文件信息 res/a 指向数据段1 60 | - 文件信息 res/b 指向数据段2 61 | - 数据段3 res/a 62 | - 数据段4 res/b 63 | - 文件信息 res/a **指向数据段3** 64 | - 文件信息 res/b **指向数据段4** 65 | 66 | **优化后** 67 | 68 | - 过签包 69 | - assets/base.apk 70 | - 数据段1 res/a 71 | - 数据段2 res/b 72 | - 文件信息 res/a 指向数据段1 73 | - 文件信息 res/b 指向数据段2 74 | - _~~数据段3 res/a(删除)~~_ 75 | - _~~数据段4 res/b(删除)~~_ 76 | - 文件信息 res/a **改为指向数据段1** 77 | - 文件信息 res/b **改为指向数据段2** 78 | 79 | 因为原包的数据不能动,因此我们删除的是过签包中对应文件的数据段,并让过签包中央目录对应文件的数据偏移指向原包对应的数据段。 80 | 81 | 这里有个非常重要的前提,**原包必须以存储方式打包到过签包中,不能压缩!!!** 82 | 83 | ## 使用方法 84 | 85 | 源码无任何依赖,方便移植与修改,可直接复制到你的项目,并调用以下方法。 86 | 87 | ``` Java 88 | DataMultiplexing.optimize("test.apk", "output.apk", "assets/base.apk", true); 89 | ``` 90 | 91 | V2 / V3 签名必须在优化之后进行,并且必须使用 `V2V3SchemeSigner`,否则优化会失效,关于签名的更多信息请看后面的说明。 92 | 93 | ``` Java 94 | V2V3SchemeSigner.sign(new File("output.apk"), new JksSignatureKey("test.jks", "123456", "123456", "123456"), true, true); 95 | ``` 96 | 97 | 另外输出的文件已经过 ZipAlign,不建议进行其它处理,否则很可能导致失去优化效果。 98 | 99 | ## 局限性 100 | 101 | 1. 根据上面的原理,优化后能减小多少体积取决于过签包与原包有多少完全相同的文件,最多不会超过 102 | 50%,这里的完全相同包括文件名相同、文件数据相同、压缩方式相同。 103 | 104 | 2. 由于该优化要求原包必须以存储方式进行打包,相比以压缩方式打包会占用更多体积,不过原包已经是一个压缩包了,再次压缩效果有限,但极端情况下也可能因为这个导致优化后体积增大。 105 | 106 | 3. 由于目前基本没有兼容该技术的工具,对优化包进行二次修改(如签名,添加或删除文件等)将大概率导致优化失效,体积会膨胀回去,需要再次进行优化。 107 | 108 | 打个广告,[MT管理器](https://mt2.cn/)已完美兼容该技术,修改此类 apk 不会导致优化失效。 109 | 110 | ## 关于签名 111 | 112 | 我们一般使用 [apksig](https://android.googlesource.com/platform/tools/apksig/) 对 apk 113 | 进行签名,然而经过测试,使用该库签名后,数据复用优化会失效,而数据复用优化又会破坏 V2 / V3 签名。 114 | 115 | 为此本项目提供了一个 `V2V3SchemeSigner` 来进行 V2 / V3 签名同时又不破坏数据复用优化。 116 | 117 | 在签名时需要遵守以下规则: 118 | 119 | ### V1 签名 120 | 121 | 先用 apksig 进行 V1 签名,然后进行数据复用优化。 122 | 123 | ### V2 / V3 签名 124 | 125 | 先进行数据复用优化,再使用 `V2V3SchemeSigner` 进行签名,不需要用到 apksig。 126 | 127 | 不需要兼容 Android 4.x 的话直接使用这个方案比较方便。 128 | 129 | ### V1 + V2 / V3 签名 130 | 131 | 先用 apksig 进行 V1 + V2 / V3 签名,然后进行数据复用优化,最后使用 `V2V3SchemeSigner` 再次签名。 132 | 133 | ## 致谢 134 | 135 | 感谢 LSP 技术团队!该技术思路并非原创,起因是前阵子有网友跟我反馈,使用 MT 修改 LSPatch 生成的 apk 136 | 文件后体积会暴涨,在分析了测试文件后才发现了这个思路。 -------------------------------------------------------------------------------- /src/bin/io/BufferedRandomAccessFile.java: -------------------------------------------------------------------------------- 1 | package bin.io; 2 | 3 | import java.io.EOFException; 4 | import java.io.IOException; 5 | import java.util.Arrays; 6 | 7 | /** 8 | * A BufferedRandomAccessFile is like a 9 | * RandomAccessFile, but it uses a private buffer so that most 10 | * operations do not require a disk access. 11 | *

12 | *

13 | * Note: The operations on this class are unmonitored. Also, the correct 14 | * functioning of the RandomAccessFile methods that are not 15 | * overridden here relies on the implementation of those methods in the 16 | * superclass. 17 | * Author : Avinash Lakshman ( alakshman@facebook.com) & Prashant Malik ( pmalik@facebook.com ) 18 | */ 19 | 20 | public final class BufferedRandomAccessFile implements RandomAccessFile { 21 | private static final int LogBuffSz_ = 17; // 128K buffer 22 | private static final int BuffSz_ = (1 << LogBuffSz_); 23 | private static final long BuffMask_ = -((long) BuffSz_); 24 | 25 | private boolean dirty_; // true iff unflushed bytes exist 26 | private boolean closed_; // true iff the file is closed 27 | private long curr_; // current position in file 28 | private long lo_, hi_; // bounds on characters in "buff" 29 | private byte[] buff_; // local buffer 30 | private long maxHi_; // this.lo + this.buff.length 31 | private boolean hitEOF_; // buffer contains last file block? 32 | private long diskPos_; // disk position 33 | private RandomAccessData randomAccessData; 34 | private long randomAccessDataLength = -1; 35 | 36 | /** 37 | * Open a new BufferedRandomAccessFile on file 38 | * in mode mode, which should be "r" for reading only, or 39 | * "rw" for reading and writing. 40 | */ 41 | BufferedRandomAccessFile(RandomAccessData randomAccessData) { 42 | this.randomAccessData = randomAccessData; 43 | this.init(); 44 | } 45 | 46 | private void init() { 47 | this.dirty_ = this.closed_ = false; 48 | this.lo_ = this.curr_ = this.hi_ = 0; 49 | this.buff_ = new byte[BuffSz_]; 50 | this.maxHi_ = (long) BuffSz_; 51 | this.hitEOF_ = false; 52 | this.diskPos_ = 0L; 53 | } 54 | 55 | @Override 56 | public void write(int value) throws IOException { 57 | if (this.curr_ >= this.hi_) { 58 | if (this.hitEOF_ && this.hi_ < this.maxHi_) { 59 | // at EOF -- bump "hi" 60 | this.hi_++; 61 | } else { 62 | // slow path -- write current buffer; read next one 63 | this.seek(this.curr_); 64 | if (this.curr_ == this.hi_) { 65 | // appending to EOF -- bump "hi" 66 | this.hi_++; 67 | } 68 | } 69 | } 70 | this.buff_[(int) (this.curr_ - this.lo_)] = (byte) value; 71 | this.curr_++; 72 | this.dirty_ = true; 73 | } 74 | 75 | @Override 76 | public void write(byte[] data) throws IOException { 77 | this.write(data, 0, data.length); 78 | } 79 | 80 | @Override 81 | public void write(byte[] data, int off, int len) throws IOException { 82 | while (len > 0) { 83 | int n = this.writeAtMost(data, off, len); 84 | off += n; 85 | len -= n; 86 | this.dirty_ = true; 87 | } 88 | } 89 | 90 | @Override 91 | public int read() throws IOException { 92 | if (this.curr_ >= this.hi_) { 93 | // test for EOF 94 | // if (this.hi < this.maxHi) return -1; 95 | if (this.hitEOF_) 96 | return -1; 97 | 98 | // slow path -- read another buffer 99 | this.seek(this.curr_); 100 | if (this.curr_ == this.hi_) 101 | return -1; 102 | } 103 | byte res = this.buff_[(int) (this.curr_ - this.lo_)]; 104 | this.curr_++; 105 | return ((int) res) & 0xFF; // convert byte -> int 106 | } 107 | 108 | @Override 109 | public int read(byte[] data) throws IOException { 110 | return read(data, 0, data.length); 111 | } 112 | 113 | @Override 114 | public int read(byte[] b, int off, int len) throws IOException { 115 | if (this.curr_ >= this.hi_) { 116 | // test for EOF 117 | // if (this.hi < this.maxHi) return -1; 118 | if (this.hitEOF_) 119 | return -1; 120 | 121 | // slow path -- read another buffer 122 | this.seek(this.curr_); 123 | if (this.curr_ == this.hi_) 124 | return -1; 125 | } 126 | len = Math.min(len, (int) (this.hi_ - this.curr_)); 127 | int buffOff = (int) (this.curr_ - this.lo_); 128 | System.arraycopy(this.buff_, buffOff, b, off, len); 129 | this.curr_ += len; 130 | return len; 131 | } 132 | 133 | @Override 134 | public void readFully(byte[] data) throws IOException { 135 | readFully(data, 0, data.length); 136 | } 137 | 138 | @Override 139 | public void readFully(byte[] data, int off, int len) throws IOException { 140 | int n = 0; 141 | do { 142 | int count = read(data, off + n, len - n); 143 | if (count < 0) 144 | throw new EOFException(); 145 | n += count; 146 | } while (n < len); 147 | } 148 | 149 | @Override 150 | public long length() throws IOException { 151 | return Math.max(this.curr_, getRandomAccessDataLength()); 152 | } 153 | 154 | private long getRandomAccessDataLength() throws IOException { 155 | if (randomAccessDataLength == -1) { 156 | randomAccessDataLength = randomAccessData.length(); 157 | } 158 | return randomAccessDataLength; 159 | } 160 | 161 | @Override 162 | public void setLength(long newLength) throws IOException { 163 | flushBuffer(); 164 | randomAccessData.setLength(newLength); 165 | randomAccessDataLength = newLength; 166 | if (this.curr_ > newLength) { 167 | this.curr_ = newLength; 168 | } 169 | if (this.diskPos_ > newLength) { 170 | randomAccessData.seek(newLength); 171 | this.diskPos_ = newLength; 172 | } 173 | 174 | // 为了fillBuffer 175 | this.lo_ = this.hi_ = 0; 176 | seek(this.curr_); 177 | } 178 | 179 | /* 180 | * This method positions this.curr at position pos. 181 | * If pos does not fall in the current buffer, it flushes the 182 | * current buffer and loads the correct one.

183 | * 184 | * On exit from this routine this.curr == this.hi iff pos 185 | * is at or past the end-of-file, which can only happen if the file was 186 | * opened in read-only mode. 187 | */ 188 | @Override 189 | public void seek(long pos) throws IOException { 190 | if (pos >= this.hi_ || pos < this.lo_) { 191 | // seeking outside of current buffer -- flush and read 192 | this.flushBuffer(); 193 | this.lo_ = pos & BuffMask_; // start at BuffSz boundary 194 | this.maxHi_ = this.lo_ + (long) this.buff_.length; 195 | if (this.diskPos_ != this.lo_) { 196 | randomAccessData.seek(this.lo_); 197 | this.diskPos_ = this.lo_; 198 | } 199 | int n = this.fillBuffer(); 200 | this.hi_ = this.lo_ + (long) n; 201 | } else { 202 | // seeking inside current buffer -- no read required 203 | if (pos < this.curr_) { 204 | // if seeking backwards, we must flush to maintain V4 205 | this.flushBuffer(); 206 | } 207 | } 208 | this.curr_ = pos; 209 | } 210 | 211 | @Override 212 | public int skipBytes(int n) throws IOException { 213 | long pos; 214 | long len; 215 | long newpos; 216 | 217 | if (n <= 0) { 218 | return 0; 219 | } 220 | pos = getFilePointer(); 221 | len = length(); 222 | newpos = pos + n; 223 | if (newpos > len) { 224 | newpos = len; 225 | } 226 | seek(newpos); 227 | 228 | /* return the actual number of bytes skipped */ 229 | return (int) (newpos - pos); 230 | } 231 | 232 | @Override 233 | public long getFilePointer() { 234 | return this.curr_; 235 | } 236 | 237 | @Override 238 | public String getName() { 239 | return randomAccessData.getName(); 240 | } 241 | 242 | @Override 243 | public RandomAccessFile getAnotherInSameParent(String name) throws IOException { 244 | return new BufferedRandomAccessFile(randomAccessData.getAnotherInSameParent(name)); 245 | } 246 | 247 | @Override 248 | public RandomAccessFile newSameInstance() throws IOException { 249 | return new BufferedRandomAccessFile(randomAccessData.newSameInstance()); 250 | } 251 | 252 | @Override 253 | public RandomAccessFile newFragment(long offset, long length) throws IOException { 254 | return new BufferedRandomAccessFile(randomAccessData.newFragment(offset, length)); 255 | } 256 | 257 | @Override 258 | public void flush() throws IOException { 259 | this.flushBuffer(); 260 | } 261 | 262 | @Override 263 | public void close() throws IOException { 264 | this.flush(); 265 | this.closed_ = true; 266 | randomAccessData.close(); 267 | } 268 | 269 | @Override 270 | public boolean isClosed() { 271 | return closed_; 272 | } 273 | 274 | /* Flush any dirty bytes in the buffer to disk. */ 275 | private void flushBuffer() throws IOException { 276 | if (this.dirty_) { 277 | if (this.diskPos_ != this.lo_) 278 | randomAccessData.seek(this.lo_); 279 | int len = (int) (this.curr_ - this.lo_); 280 | randomAccessData.write(this.buff_, 0, len); 281 | this.diskPos_ = this.curr_; 282 | this.dirty_ = false; 283 | if (randomAccessDataLength != -1 && this.diskPos_ > randomAccessDataLength) { 284 | randomAccessDataLength = -1; 285 | } 286 | } 287 | } 288 | 289 | /* 290 | * Read at most "this.buff.length" bytes into "this.buff", returning the 291 | * number of bytes read. If the return result is less than 292 | * "this.buff.length", then EOF was read. 293 | */ 294 | private int fillBuffer() throws IOException { 295 | int cnt = 0; 296 | int rem = this.buff_.length; 297 | while (rem > 0) { 298 | int n = randomAccessData.read(this.buff_, cnt, rem); 299 | if (n < 0) 300 | break; 301 | cnt += n; 302 | rem -= n; 303 | } 304 | if (this.hitEOF_ = (cnt < this.buff_.length)) { 305 | // make sure buffer that wasn't read is initialized with -1 306 | Arrays.fill(this.buff_, cnt, this.buff_.length, (byte) 0xff); 307 | } 308 | this.diskPos_ += cnt; 309 | return cnt; 310 | } 311 | 312 | /* 313 | * Write at most "len" bytes to "b" starting at position "off", and return 314 | * the number of bytes written. 315 | */ 316 | private int writeAtMost(byte[] b, int off, int len) throws IOException { 317 | if (this.curr_ >= this.hi_) { 318 | if (this.hitEOF_ && this.hi_ < this.maxHi_) { 319 | // at EOF -- bump "hi" 320 | this.hi_ = this.maxHi_; 321 | } else { 322 | // slow path -- write current buffer; read next one 323 | this.seek(this.curr_); 324 | if (this.curr_ == this.hi_) { 325 | // appending to EOF -- bump "hi" 326 | this.hi_ = this.maxHi_; 327 | } 328 | } 329 | } 330 | len = Math.min(len, (int) (this.hi_ - this.curr_)); 331 | int buffOff = (int) (this.curr_ - this.lo_); 332 | System.arraycopy(b, off, this.buff_, buffOff, len); 333 | this.curr_ += len; 334 | return len; 335 | } 336 | 337 | } -------------------------------------------------------------------------------- /src/bin/io/FragmentRandomAccessData.java: -------------------------------------------------------------------------------- 1 | package bin.io; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * @author Bin 7 | */ 8 | class FragmentRandomAccessData implements RandomAccessData { 9 | private final RandomAccessData randomAccessData; 10 | private final long offset; 11 | private final long length; 12 | private long pos; 13 | 14 | public FragmentRandomAccessData(RandomAccessData randomAccessData, long offset, long length) throws IOException { 15 | this.randomAccessData = randomAccessData; 16 | this.offset = offset; 17 | this.length = length; 18 | long dataLength = randomAccessData.length(); 19 | if (offset + length > dataLength) { 20 | throw new IOException(String.format("fragment.offset=%d, fragment.length=%d, data.length=%d", offset, length, dataLength)); 21 | } 22 | seek(0); 23 | } 24 | 25 | @Override 26 | public void seek(long pos) throws IOException { 27 | randomAccessData.seek(pos + offset); 28 | this.pos = randomAccessData.position() - offset; 29 | } 30 | 31 | @Override 32 | public int read(byte[] data, int off, int len) throws IOException { 33 | long available = length - pos; 34 | if (len > available) { 35 | if (available <= 0) { 36 | return -1; 37 | } 38 | len = (int) available; 39 | } 40 | int readLen = randomAccessData.read(data, off, len); 41 | if (readLen > 0) { 42 | this.pos += readLen; 43 | } 44 | return readLen; 45 | } 46 | 47 | @Override 48 | public void write(byte[] data, int off, int len) throws IOException { 49 | throw new IOException("FragmentRandomAccessData is readonly"); 50 | } 51 | 52 | @Override 53 | public long length() throws IOException { 54 | return length; 55 | } 56 | 57 | @Override 58 | public void setLength(long newLength) throws IOException { 59 | throw new IOException("FragmentRandomAccessData is readonly"); 60 | } 61 | 62 | @Override 63 | public long position() throws IOException { 64 | return pos; 65 | } 66 | 67 | @Override 68 | public void sync() throws IOException { 69 | randomAccessData.sync(); 70 | } 71 | 72 | @Override 73 | public String getName() { 74 | return randomAccessData.getName() + "-Fragment(" + offset + "," + length + ")"; 75 | } 76 | 77 | @Override 78 | public RandomAccessData getAnotherInSameParent(String name) throws IOException { 79 | throw new IOException("Unsupported"); 80 | } 81 | 82 | @Override 83 | public RandomAccessData newSameInstance() throws IOException { 84 | return new FragmentRandomAccessData(randomAccessData.newSameInstance(), offset, length); 85 | } 86 | 87 | @Override 88 | public void close() throws IOException { 89 | randomAccessData.close(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/bin/io/RandomAccessData.java: -------------------------------------------------------------------------------- 1 | package bin.io; 2 | 3 | import java.io.Closeable; 4 | import java.io.IOException; 5 | 6 | /** 7 | * @author Bin 8 | */ 9 | public interface RandomAccessData extends Closeable { 10 | 11 | void seek(long pos) throws IOException; 12 | 13 | int read(byte[] data, int off, int len) throws IOException; 14 | 15 | void write(byte[] data, int off, int len) throws IOException; 16 | 17 | long length() throws IOException; 18 | 19 | void setLength(long newLength) throws IOException; 20 | 21 | long position() throws IOException; 22 | 23 | void sync() throws IOException; 24 | 25 | String getName(); 26 | 27 | RandomAccessData getAnotherInSameParent(String name) throws IOException; 28 | 29 | RandomAccessData newSameInstance() throws IOException; 30 | 31 | default RandomAccessData newFragment(long offset, long length) throws IOException { 32 | return new FragmentRandomAccessData(newSameInstance(), offset, length); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/bin/io/RandomAccessDataImpl.java: -------------------------------------------------------------------------------- 1 | package bin.io; 2 | 3 | import java.io.File; 4 | import java.io.FileNotFoundException; 5 | import java.io.IOException; 6 | import java.io.RandomAccessFile; 7 | 8 | /** 9 | * @author Bin 10 | */ 11 | class RandomAccessDataImpl implements RandomAccessData { 12 | private final RandomAccessFile randomAccessFile; 13 | private final File file; 14 | private final String mode; 15 | 16 | RandomAccessDataImpl(String path, String mode) throws FileNotFoundException { 17 | this(new File(path), mode); 18 | } 19 | 20 | RandomAccessDataImpl(File file, String mode) throws FileNotFoundException { 21 | this.file = file; 22 | this.mode = mode; 23 | this.randomAccessFile = new RandomAccessFile(file, mode); 24 | } 25 | 26 | @Override 27 | public void seek(long pos) throws IOException { 28 | randomAccessFile.seek(pos); 29 | } 30 | 31 | @Override 32 | public int read(byte[] data, int off, int len) throws IOException { 33 | return randomAccessFile.read(data, off, len); 34 | } 35 | 36 | @Override 37 | public void write(byte[] data, int off, int len) throws IOException { 38 | randomAccessFile.write(data, off, len); 39 | } 40 | 41 | @Override 42 | public long length() throws IOException { 43 | return randomAccessFile.length(); 44 | } 45 | 46 | @Override 47 | public void setLength(long newLength) throws IOException { 48 | randomAccessFile.setLength(newLength); 49 | } 50 | 51 | @Override 52 | public long position() throws IOException { 53 | return randomAccessFile.getFilePointer(); 54 | } 55 | 56 | @Override 57 | public void sync() throws IOException { 58 | randomAccessFile.getFD().sync(); 59 | } 60 | 61 | @Override 62 | public String getName() { 63 | return file.getName(); 64 | } 65 | 66 | @Override 67 | public RandomAccessData getAnotherInSameParent(String name) throws IOException { 68 | File another = new File(file.getParent(), name); 69 | return new RandomAccessDataImpl(another, mode); 70 | } 71 | 72 | @Override 73 | public RandomAccessData newSameInstance() throws IOException { 74 | return new RandomAccessDataImpl(file, mode); 75 | } 76 | 77 | @Override 78 | public void close() throws IOException { 79 | randomAccessFile.close(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/bin/io/RandomAccessFactory.java: -------------------------------------------------------------------------------- 1 | package bin.io; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | 6 | /** 7 | * @author Bin 8 | */ 9 | public class RandomAccessFactory { 10 | 11 | public static RandomAccessFile from(RandomAccessData randomAccessData) { 12 | return new BufferedRandomAccessFile(randomAccessData); 13 | } 14 | 15 | public static RandomAccessFile from(File file, String mode) throws IOException { 16 | return new BufferedRandomAccessFile(new RandomAccessDataImpl(file, mode)); 17 | } 18 | 19 | public static RandomAccessFile from(String path, String mode) throws IOException { 20 | return new BufferedRandomAccessFile(new RandomAccessDataImpl(path, mode)); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/bin/io/RandomAccessFile.java: -------------------------------------------------------------------------------- 1 | package bin.io; 2 | 3 | import java.io.Closeable; 4 | import java.io.EOFException; 5 | import java.io.IOException; 6 | 7 | /** 8 | * @author Bin 9 | */ 10 | public interface RandomAccessFile extends Closeable { 11 | 12 | void write(int value) throws IOException; 13 | 14 | void write(byte[] data) throws IOException; 15 | 16 | void write(byte[] data, int off, int len) throws IOException; 17 | 18 | int read() throws IOException; 19 | 20 | int read(byte[] data) throws IOException; 21 | 22 | int read(byte[] data, int off, int len) throws IOException; 23 | 24 | void readFully(byte[] data) throws IOException; 25 | 26 | void readFully(byte[] data, int off, int len) throws IOException; 27 | 28 | long length() throws IOException; 29 | 30 | void setLength(long newLength) throws IOException; 31 | 32 | void seek(long pos) throws IOException; 33 | 34 | int skipBytes(int n) throws IOException; 35 | 36 | long getFilePointer() throws IOException; 37 | 38 | String getName(); 39 | 40 | RandomAccessFile getAnotherInSameParent(String name) throws IOException; 41 | 42 | RandomAccessFile newSameInstance() throws IOException; 43 | 44 | RandomAccessFile newFragment(long offset, long length) throws IOException; 45 | 46 | void flush() throws IOException; 47 | 48 | boolean isClosed(); 49 | 50 | default void writeByte(byte b) throws IOException { 51 | write(b); 52 | } 53 | 54 | default void writeUShort(int i) throws IOException { 55 | write(i & 0xFF); 56 | write(i >>> 8 & 0xFF); 57 | } 58 | 59 | default void writeShort(short i) throws IOException { 60 | writeUShort(i); 61 | } 62 | 63 | default void writeChar(char c) throws IOException { 64 | writeUShort(c); 65 | } 66 | 67 | default void writeInt(int i) throws IOException { 68 | write(i & 0xFF); 69 | write(i >>> 8 & 0xFF); 70 | write(i >>> 16 & 0xFF); 71 | write(i >>> 24 & 0xFF); 72 | } 73 | 74 | default void writeLong(long l) throws IOException { 75 | write((int) (l & 0xFF)); 76 | write((int) (l >>> 8 & 0xFF)); 77 | write((int) (l >>> 16 & 0xFF)); 78 | write((int) (l >>> 24 & 0xFF)); 79 | write((int) (l >>> 32 & 0xFF)); 80 | write((int) (l >>> 40 & 0xFF)); 81 | write((int) (l >>> 48 & 0xFF)); 82 | write((int) (l >>> 56 & 0xFF)); 83 | } 84 | 85 | default byte readByte() throws IOException { 86 | int ret = read(); 87 | if (ret == -1) { 88 | throw new EOFException(); 89 | } 90 | return (byte) ret; 91 | } 92 | 93 | default int readUShort() throws IOException { 94 | return readByte() & 0xFF | (readByte() & 0xFF) << 8; 95 | } 96 | 97 | default short readShort() throws IOException { 98 | return (short) readUShort(); 99 | } 100 | 101 | default char readChar() throws IOException { 102 | return (char) readUShort(); 103 | } 104 | 105 | default int readInt() throws IOException { 106 | return readByte() & 0xFF | (readByte() & 0xFF) << 8 | (readByte() & 0xFF) << 16 | (readByte() & 0xFF) << 24; 107 | } 108 | 109 | default long readLong() throws IOException { 110 | return readByte() & 0xFFL | (readByte() & 0xFFL) << 8 | (readByte() & 0xFFL) << 16 | (readByte() & 0xFFL) << 24 111 | | (readByte() & 0xFFL) << 32 | (readByte() & 0xFFL) << 40 | (readByte() & 0xFFL) << 48 | (readByte() & 0xFFL) << 56; 112 | } 113 | 114 | 115 | } -------------------------------------------------------------------------------- /src/bin/mt/apksign/ByteArrayUtil.java: -------------------------------------------------------------------------------- 1 | package bin.mt.apksign; 2 | 3 | class ByteArrayUtil { 4 | 5 | static void setInt(int value, byte[] data, int offset) { 6 | data[offset] = (byte) (value & 0xff); 7 | data[offset + 1] = (byte) ((value >> 8) & 0xff); 8 | data[offset + 2] = (byte) ((value >> 16) & 0xff); 9 | data[offset + 3] = (byte) ((value >> 24) & 0xff); 10 | } 11 | 12 | static void setUInt(long value, byte[] data, int offset) { 13 | data[offset] = (byte) (value & 0xff); 14 | data[offset + 1] = (byte) ((value >> 8) & 0xff); 15 | data[offset + 2] = (byte) ((value >> 16) & 0xff); 16 | data[offset + 3] = (byte) ((value >> 24) & 0xff); 17 | } 18 | 19 | static void setLong(long value, byte[] data, int offset) { 20 | data[offset] = (byte) (value & 0xff); 21 | data[offset + 1] = (byte) ((value >> 8) & 0xff); 22 | data[offset + 2] = (byte) ((value >> 16) & 0xff); 23 | data[offset + 3] = (byte) ((value >> 24) & 0xff); 24 | data[offset + 4] = (byte) ((value >> 32) & 0xff); 25 | data[offset + 5] = (byte) ((value >> 40) & 0xff); 26 | data[offset + 6] = (byte) ((value >> 48) & 0xff); 27 | data[offset + 7] = (byte) ((value >> 56) & 0xff); 28 | } 29 | 30 | static long readUInt(byte[] data, int offset) { 31 | long ch1 = data[offset] & 0xffL; 32 | long ch2 = data[offset + 1] & 0xffL; 33 | long ch3 = data[offset + 2] & 0xffL; 34 | long ch4 = data[offset + 3] & 0xffL; 35 | return (ch1) | (ch2 << 8) | (ch3 << 16) | (ch4 << 24); 36 | } 37 | 38 | static byte[] intToBytes(int value) { 39 | byte[] array = new byte[4]; 40 | setInt(value, array, 0); 41 | return array; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/bin/mt/apksign/SignatureAlgorithm.java: -------------------------------------------------------------------------------- 1 | package bin.mt.apksign; 2 | 3 | import bin.mt.apksign.data.DataSource; 4 | 5 | import java.io.IOException; 6 | import java.io.OutputStream; 7 | import java.security.*; 8 | import java.security.spec.AlgorithmParameterSpec; 9 | import java.security.spec.MGF1ParameterSpec; 10 | import java.security.spec.PSSParameterSpec; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | import java.util.function.Supplier; 14 | 15 | import static bin.mt.apksign.ByteArrayUtil.setInt; 16 | 17 | abstract class SignatureAlgorithm { 18 | protected static final Map> MAP = new HashMap<>(); 19 | private static final int ONE_MB = 1024 * 1024; 20 | protected byte[] digest; 21 | protected byte[] signature; 22 | 23 | static { 24 | MAP.put(0x0101, SignatureAlgorithm::RSA_PSS_WITH_SHA256); 25 | MAP.put(0x0102, SignatureAlgorithm::RSA_PSS_WITH_SHA512); 26 | MAP.put(0x0103, SignatureAlgorithm::RSA_PKCS1_V1_5_WITH_SHA256); 27 | MAP.put(0x0104, SignatureAlgorithm::RSA_PKCS1_V1_5_WITH_SHA512); 28 | MAP.put(0x0201, SignatureAlgorithm::ECDSA_WITH_SHA256); 29 | MAP.put(0x0202, SignatureAlgorithm::ECDSA_WITH_SHA512); 30 | MAP.put(0x0301, SignatureAlgorithm::DSA_WITH_SHA256); 31 | MAP.put(0x0421, SignatureAlgorithm::VERITY_RSA_PKCS1_V1_5_WITH_SHA256); 32 | MAP.put(0x0423, SignatureAlgorithm::VERITY_ECDSA_WITH_SHA256); 33 | MAP.put(0x0425, SignatureAlgorithm::VERITY_DSA_WITH_SHA256); 34 | } 35 | 36 | static boolean isAlgorithmIdSupported(int id) { 37 | return MAP.containsKey(id); 38 | } 39 | 40 | static SignatureAlgorithm getByAlgorithmId(int id) { 41 | Supplier supplier = MAP.get(id); 42 | if (supplier == null) { 43 | throw new RuntimeException("Unsupported signature algorithm id: 0x" + Integer.toHexString(id)); 44 | } 45 | return supplier.get(); 46 | } 47 | 48 | static SignatureAlgorithm findByAlgorithmId(int id) { 49 | Supplier supplier = MAP.get(id); 50 | if (supplier == null) { 51 | return null; 52 | } 53 | return supplier.get(); 54 | } 55 | 56 | static SignatureAlgorithm RSA_PSS_WITH_SHA256() { 57 | return new BaseSignatureAlgorithm(0x0101, "SHA-256", "RSA", "SHA256withRSA/PSS", 58 | new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)); 59 | } 60 | 61 | static SignatureAlgorithm RSA_PSS_WITH_SHA512() { 62 | return new BaseSignatureAlgorithm(0x0102, "SHA-512", "RSA", "SHA512withRSA/PSS", 63 | new PSSParameterSpec("SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)); 64 | } 65 | 66 | static SignatureAlgorithm RSA_PKCS1_V1_5_WITH_SHA256() { 67 | return new BaseSignatureAlgorithm(0x0103, "SHA-256", "RSA", "SHA256withRSA", null); 68 | } 69 | 70 | static SignatureAlgorithm RSA_PKCS1_V1_5_WITH_SHA512() { 71 | return new BaseSignatureAlgorithm(0x0104, "SHA-512", "RSA", "SHA512withRSA", null); 72 | } 73 | 74 | static SignatureAlgorithm ECDSA_WITH_SHA256() { 75 | return new BaseSignatureAlgorithm(0x0201, "SHA-256", "EC", "SHA256withECDSA", null); 76 | } 77 | 78 | static SignatureAlgorithm ECDSA_WITH_SHA512() { 79 | return new BaseSignatureAlgorithm(0x0202, "SHA-256", "EC", "SHA512withECDSA", null); 80 | } 81 | 82 | static SignatureAlgorithm DSA_WITH_SHA256() { 83 | return new BaseSignatureAlgorithm(0x0301, "SHA-256", "DSA", "SHA256withDSA", null); 84 | } 85 | 86 | static SignatureAlgorithm VERITY_RSA_PKCS1_V1_5_WITH_SHA256() { 87 | return new BaseVeritySignatureAlgorithm(0x0421, "RSA", "SHA256withRSA", null); 88 | } 89 | 90 | static SignatureAlgorithm VERITY_ECDSA_WITH_SHA256() { 91 | return new BaseVeritySignatureAlgorithm(0x0423, "EC", "SHA256withECDSA", null); 92 | } 93 | 94 | static SignatureAlgorithm VERITY_DSA_WITH_SHA256() { 95 | return new BaseVeritySignatureAlgorithm(0x0425, "DSA", "SHA256withDSA", null); 96 | } 97 | 98 | private static void updateChunkContentDigest(MessageDigest contentDigest, DataSource dataSource, 99 | OutputStream output) throws IOException { 100 | int chunkCount = getChunkCount(dataSource.size()); 101 | 102 | byte[] chunkContentPrefix = new byte[5]; 103 | chunkContentPrefix[0] = (byte) 0xa5; 104 | for (int i = 0; i < chunkCount; i++) { 105 | long start = dataSource.pos(); 106 | long end = Math.min(start + ONE_MB, dataSource.size()); 107 | int chunkSize = (int) (end - start); 108 | setInt(chunkSize, chunkContentPrefix, 1); 109 | 110 | contentDigest.update(chunkContentPrefix); 111 | dataSource.copyTo(contentDigest, chunkSize); 112 | 113 | byte[] digest = contentDigest.digest(); 114 | // PrintUtil.printDigest(digest); 115 | output.write(digest); 116 | } 117 | } 118 | 119 | private static int getChunkCount(long inputSize) { 120 | return (int) ((inputSize + ONE_MB - 1) / ONE_MB); 121 | } 122 | 123 | public abstract int getId(); 124 | 125 | public abstract int getMinSdkVersion(); 126 | 127 | public abstract String getKeyAlgorithm(); 128 | 129 | public abstract String getSignatureAlgorithm(); 130 | 131 | public abstract AlgorithmParameterSpec getSignatureAlgorithmParams(); 132 | 133 | public abstract void computeDigest(DataSource beforeCentralDir, DataSource centralDir, 134 | DataSource eocd) throws Exception; 135 | 136 | boolean verifySignature(PublicKey publicKey, byte[] signedData, byte[] signatureBytes) throws Exception { 137 | String jcaSignatureAlgorithm = getSignatureAlgorithm(); 138 | AlgorithmParameterSpec jcaSignatureAlgorithmParams = getSignatureAlgorithmParams(); 139 | try { 140 | Signature signature = Signature.getInstance(jcaSignatureAlgorithm); 141 | signature.initVerify(publicKey); 142 | if (jcaSignatureAlgorithmParams != null) { 143 | signature.setParameter(jcaSignatureAlgorithmParams); 144 | } 145 | signature.update(signedData); 146 | return signature.verify(signatureBytes); 147 | } catch (InvalidKeyException e) { 148 | throw new InvalidKeyException( 149 | "Failed to verify generated " + jcaSignatureAlgorithm + " signature using" 150 | + " public key from certificate", e); 151 | } catch (InvalidAlgorithmParameterException | SignatureException e) { 152 | throw new SignatureException( 153 | "Failed to verify generated " + jcaSignatureAlgorithm + " signature using" 154 | + " public key from certificate", e); 155 | } 156 | } 157 | 158 | void computeSignature(PrivateKey privateKey, PublicKey publicKey, byte[] signedData) throws Exception { 159 | String jcaSignatureAlgorithm = getSignatureAlgorithm(); 160 | AlgorithmParameterSpec jcaSignatureAlgorithmParams = getSignatureAlgorithmParams(); 161 | byte[] signatureBytes; 162 | try { 163 | Signature signature = Signature.getInstance(jcaSignatureAlgorithm); 164 | signature.initSign(privateKey); 165 | if (jcaSignatureAlgorithmParams != null) { 166 | signature.setParameter(jcaSignatureAlgorithmParams); 167 | } 168 | signature.update(signedData); 169 | signatureBytes = signature.sign(); 170 | } catch (InvalidKeyException e) { 171 | throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e); 172 | } catch (InvalidAlgorithmParameterException | SignatureException e) { 173 | throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e); 174 | } 175 | try { 176 | Signature signature = Signature.getInstance(jcaSignatureAlgorithm); 177 | signature.initVerify(publicKey); 178 | if (jcaSignatureAlgorithmParams != null) { 179 | signature.setParameter(jcaSignatureAlgorithmParams); 180 | } 181 | signature.update(signedData); 182 | if (!signature.verify(signatureBytes)) { 183 | throw new SignatureException("Failed to verify generated " 184 | + jcaSignatureAlgorithm 185 | + " signature using public key from certificate"); 186 | } 187 | } catch (InvalidKeyException e) { 188 | throw new InvalidKeyException( 189 | "Failed to verify generated " + jcaSignatureAlgorithm + " signature using" 190 | + " public key from certificate", e); 191 | } catch (InvalidAlgorithmParameterException | SignatureException e) { 192 | throw new SignatureException( 193 | "Failed to verify generated " + jcaSignatureAlgorithm + " signature using" 194 | + " public key from certificate", e); 195 | } 196 | signature = signatureBytes; 197 | } 198 | 199 | public byte[] getDigest() { 200 | return digest; 201 | } 202 | 203 | public byte[] getSignature() { 204 | return signature; 205 | } 206 | 207 | static class BaseSignatureAlgorithm extends SignatureAlgorithm { 208 | private int id; 209 | private String digestAlgorithm; 210 | private String keyAlgorithm; 211 | private String signatureAlgorithm; 212 | private AlgorithmParameterSpec signatureAlgorithmParams; 213 | 214 | BaseSignatureAlgorithm(int id, String digestAlgorithm, String keyAlgorithm, String signatureAlgorithm, AlgorithmParameterSpec signatureAlgorithmParams) { 215 | this.id = id; 216 | this.digestAlgorithm = digestAlgorithm; 217 | this.keyAlgorithm = keyAlgorithm; 218 | this.signatureAlgorithm = signatureAlgorithm; 219 | this.signatureAlgorithmParams = signatureAlgorithmParams; 220 | } 221 | 222 | @Override 223 | public int getId() { 224 | return id; 225 | } 226 | 227 | @Override 228 | public int getMinSdkVersion() { 229 | return 24; 230 | } 231 | 232 | @Override 233 | public String getKeyAlgorithm() { 234 | return keyAlgorithm; 235 | } 236 | 237 | @Override 238 | public String getSignatureAlgorithm() { 239 | return signatureAlgorithm; 240 | } 241 | 242 | @Override 243 | public AlgorithmParameterSpec getSignatureAlgorithmParams() { 244 | return signatureAlgorithmParams; 245 | } 246 | 247 | @Override 248 | public void computeDigest(DataSource beforeCentralDir, DataSource centralDir, DataSource eocd) throws Exception { 249 | MessageDigest messageDigest1 = MessageDigest.getInstance(digestAlgorithm); 250 | MessageDigest messageDigest2 = MessageDigest.getInstance(digestAlgorithm); 251 | int totalChunkSize = getChunkCount(beforeCentralDir.size()) + getChunkCount(centralDir.size()) + getChunkCount(eocd.size()); 252 | OutputStream baos = new OutputStream() { 253 | @Override 254 | public void write(int b) { 255 | messageDigest2.update((byte) b); 256 | } 257 | 258 | @Override 259 | public void write(byte[] b, int off, int len) { 260 | messageDigest2.update(b, off, len); 261 | } 262 | }; 263 | byte[] prefix = new byte[5]; 264 | prefix[0] = (byte) 0x5a; 265 | setInt(totalChunkSize, prefix, 1); 266 | baos.write(prefix); 267 | 268 | updateChunkContentDigest(messageDigest1, beforeCentralDir, baos); 269 | updateChunkContentDigest(messageDigest1, centralDir, baos); 270 | updateChunkContentDigest(messageDigest1, eocd, baos); 271 | 272 | digest = messageDigest2.digest(); 273 | } 274 | } 275 | 276 | static class BaseVeritySignatureAlgorithm extends SignatureAlgorithm { 277 | private int id; 278 | private String signatureAlgorithm; 279 | private String keyAlgorithm; 280 | private AlgorithmParameterSpec signatureAlgorithmParams; 281 | 282 | BaseVeritySignatureAlgorithm(int id, String keyAlgorithm, String signatureAlgorithm, AlgorithmParameterSpec signatureAlgorithmParams) { 283 | this.id = id; 284 | this.keyAlgorithm = keyAlgorithm; 285 | this.signatureAlgorithm = signatureAlgorithm; 286 | this.signatureAlgorithmParams = signatureAlgorithmParams; 287 | } 288 | 289 | @Override 290 | public int getId() { 291 | return id; 292 | } 293 | 294 | @Override 295 | public int getMinSdkVersion() { 296 | return 28; 297 | } 298 | 299 | @Override 300 | public String getKeyAlgorithm() { 301 | return keyAlgorithm; 302 | } 303 | 304 | @Override 305 | public String getSignatureAlgorithm() { 306 | return signatureAlgorithm; 307 | } 308 | 309 | @Override 310 | public AlgorithmParameterSpec getSignatureAlgorithmParams() { 311 | return signatureAlgorithmParams; 312 | } 313 | 314 | @Override 315 | public void computeDigest(DataSource beforeCentralDir, DataSource centralDir, DataSource eocd) throws Exception { 316 | VerityTreeBuilder builder = new VerityTreeBuilder(new byte[8]); 317 | byte[] rootHash = builder.generateVerityTreeRootHash(beforeCentralDir, centralDir, eocd); 318 | byte[] result = new byte[rootHash.length + 8]; 319 | System.arraycopy(rootHash, 0, result, 0, rootHash.length); 320 | long size = beforeCentralDir.size() + centralDir.size() + eocd.size(); 321 | ByteArrayUtil.setLong(size, result, rootHash.length); 322 | digest = result; 323 | } 324 | } 325 | 326 | } 327 | -------------------------------------------------------------------------------- /src/bin/mt/apksign/V2V3SchemeSigner.java: -------------------------------------------------------------------------------- 1 | package bin.mt.apksign; 2 | 3 | import bin.io.RandomAccessFactory; 4 | import bin.io.RandomAccessFile; 5 | import bin.mt.apksign.data.ByteArrayDataSource; 6 | import bin.mt.apksign.data.DataSource; 7 | import bin.mt.apksign.data.DataSources; 8 | import bin.mt.apksign.key.SignatureKey; 9 | 10 | import java.io.File; 11 | import java.security.InvalidKeyException; 12 | import java.security.KeyFactory; 13 | import java.security.NoSuchAlgorithmException; 14 | import java.security.PublicKey; 15 | import java.security.cert.CertificateEncodingException; 16 | import java.security.cert.X509Certificate; 17 | import java.security.interfaces.RSAKey; 18 | import java.security.spec.InvalidKeySpecException; 19 | import java.security.spec.X509EncodedKeySpec; 20 | import java.util.*; 21 | 22 | import static bin.mt.apksign.ByteArrayUtil.*; 23 | 24 | /** 25 | * @author Bin 26 | */ 27 | public class V2V3SchemeSigner { 28 | private static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 0x1000; 29 | private static final int VERITY_PADDING_BLOCK_ID = 0x42726577; 30 | private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; 31 | private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0; 32 | 33 | public static void sign(File file, SignatureKey signatureKey, boolean enableV2, boolean enableV3) throws Exception { 34 | if (!enableV2 && !enableV3) { 35 | throw new RuntimeException(); 36 | } 37 | 38 | PublicKey publicKey = signatureKey.getCertificate().getPublicKey(); 39 | 40 | // Algorithms 41 | List algorithms = getSuggestedSignatureAlgorithms(publicKey); 42 | 43 | try (RandomAccessFile accessFile = RandomAccessFactory.from(file, "rw")) { 44 | ZipBuffer zipBuffer = new ZipBuffer(accessFile); 45 | 46 | // DataSource 47 | DataSource beforeCentralDir = DataSources 48 | .fromFile(accessFile, 0, zipBuffer.getEntriesDataSizeBytes()) 49 | .align(ANDROID_COMMON_PAGE_ALIGNMENT_BYTES); 50 | 51 | long start = zipBuffer.getCentralDirectoryOffset(); 52 | long size = zipBuffer.getCentralDirectorySizeBytes(); 53 | DataSource centralDir = DataSources.fromFile(accessFile, start, size).toMemory(); 54 | 55 | start = zipBuffer.getEocdOffset(); 56 | size = zipBuffer.length() - start; 57 | ByteArrayDataSource eocd = DataSources.fromFile(accessFile, start, size).toMemory(); 58 | int dif = (int) (zipBuffer.getCentralDirectoryOffset() - beforeCentralDir.size()); 59 | if (dif != 0) { 60 | // beforeCentralDir经过align后size发生变化,这里需要修复cd偏移 61 | byte[] eocdData = eocd.getBuffer(); 62 | int valueOffset = eocd.getStart() + 16; 63 | long cdOffset = readUInt(eocdData, valueOffset); 64 | cdOffset -= dif; 65 | setUInt(cdOffset, eocdData, valueOffset); 66 | } 67 | 68 | boolean reset = false; 69 | for (SignatureAlgorithm algorithm : algorithms) { 70 | if (reset) 71 | DataSources.reset(beforeCentralDir, centralDir, eocd); 72 | algorithm.computeDigest(beforeCentralDir, centralDir, eocd); 73 | reset = true; 74 | } 75 | byte[] apkSignatureSchemeV2Block = null; 76 | byte[] apkSignatureSchemeV3Block = null; 77 | 78 | if (enableV2) { 79 | byte[] v2SignedData = concat( 80 | encodeDigestPart(algorithms), 81 | encodeCertificatePart(signatureKey.getCertificate()), 82 | encodeAdditionalPart(), 83 | new byte[4] // length(int32) + byte[0] = 4byte 84 | ); 85 | for (SignatureAlgorithm algorithm : algorithms) { 86 | algorithm.computeSignature(signatureKey.getPrivateKey(), publicKey, v2SignedData); 87 | } 88 | byte[] signature = encodeSignature(algorithms); 89 | byte[] encodedPublicKey = encodePublicKey(publicKey); 90 | int v2Length = v2SignedData.length + signature.length + encodedPublicKey.length + 12; 91 | apkSignatureSchemeV2Block = concat( 92 | intToBytes(v2Length + 4), 93 | intToBytes(v2Length), 94 | intToBytes(v2SignedData.length), 95 | v2SignedData, 96 | intToBytes(signature.length), 97 | signature, 98 | intToBytes(encodedPublicKey.length), 99 | encodedPublicKey 100 | ); 101 | } 102 | if (enableV3) { 103 | byte[] v3SignedData = concat( 104 | encodeDigestPart(algorithms), 105 | encodeCertificatePart(signatureKey.getCertificate()), 106 | intToBytes(28), // minSDK 107 | intToBytes(Integer.MAX_VALUE), // maxSDK 108 | encodeAdditionalPart() 109 | ); 110 | for (SignatureAlgorithm algorithm : algorithms) { 111 | algorithm.computeSignature(signatureKey.getPrivateKey(), publicKey, v3SignedData); 112 | } 113 | byte[] signature = encodeSignature(algorithms); 114 | byte[] encodedPublicKey = encodePublicKey(publicKey); 115 | int v3Length = v3SignedData.length + signature.length + encodedPublicKey.length + 20; 116 | apkSignatureSchemeV3Block = concat( 117 | intToBytes(v3Length + 4), 118 | intToBytes(v3Length), 119 | intToBytes(v3SignedData.length), 120 | v3SignedData, 121 | intToBytes(28), // minSDK 122 | intToBytes(Integer.MAX_VALUE), // maxSDK 123 | intToBytes(signature.length), 124 | signature, 125 | intToBytes(encodedPublicKey.length), 126 | encodedPublicKey 127 | ); 128 | } 129 | // final data in zip 130 | int v2BlocksSize = !enableV2 ? 0 : 8 + 4 + apkSignatureSchemeV2Block.length; // size + id + value 131 | int v3BlocksSize = !enableV3 ? 0 : 8 + 4 + apkSignatureSchemeV3Block.length; // size + id + value 132 | int resultSize = 8 + v2BlocksSize + v3BlocksSize + 8 + 16; // size blocksSize size magic 133 | byte[] paddingPair = null; 134 | if (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) { 135 | int padding = ANDROID_COMMON_PAGE_ALIGNMENT_BYTES - 136 | (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES); 137 | if (padding < 12) { // minimum size of an ID-value pair 138 | padding += ANDROID_COMMON_PAGE_ALIGNMENT_BYTES; 139 | } 140 | paddingPair = new byte[padding]; 141 | setLong(padding - 8, paddingPair, 0); 142 | setInt(VERITY_PADDING_BLOCK_ID, paddingPair, 8); 143 | resultSize += padding; 144 | } 145 | byte[] result = new byte[resultSize]; 146 | long blockSizeFieldValue = resultSize - 8L; 147 | int pos = 0; 148 | 149 | // size 150 | setLong(blockSizeFieldValue, result, pos); 151 | pos += 8; 152 | 153 | if (enableV2) { 154 | // v2 block size 155 | setLong(4 + apkSignatureSchemeV2Block.length, result, pos); 156 | pos += 8; 157 | 158 | // v2 block id 159 | setInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID, result, pos); 160 | pos += 4; 161 | 162 | // v2 block data 163 | System.arraycopy(apkSignatureSchemeV2Block, 0, result, pos, apkSignatureSchemeV2Block.length); 164 | pos += apkSignatureSchemeV2Block.length; 165 | } 166 | 167 | if (enableV3) { 168 | // v3 block size 169 | setLong(4 + apkSignatureSchemeV3Block.length, result, pos); 170 | pos += 8; 171 | 172 | // v3 block id 173 | setInt(APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result, pos); 174 | pos += 4; 175 | 176 | // v3 block data 177 | System.arraycopy(apkSignatureSchemeV3Block, 0, result, pos, apkSignatureSchemeV3Block.length); 178 | pos += apkSignatureSchemeV3Block.length; 179 | } 180 | 181 | // padding 182 | if (paddingPair != null) { 183 | System.arraycopy(paddingPair, 0, result, pos, paddingPair.length); 184 | pos += paddingPair.length; 185 | } 186 | 187 | setLong(blockSizeFieldValue, result, pos); 188 | pos += 8; 189 | 190 | setLong(ZipBuffer.APK_SIG_BLOCK_MAGIC_LO, result, pos); 191 | pos += 8; 192 | 193 | setLong(ZipBuffer.APK_SIG_BLOCK_MAGIC_HI, result, pos); 194 | pos += 8; 195 | 196 | if (pos != resultSize) { 197 | throw new IllegalStateException(); 198 | } 199 | int padSizeBeforeApkSigningBlock = getPaddingSize(zipBuffer.getEntriesDataSizeBytes(), ANDROID_COMMON_PAGE_ALIGNMENT_BYTES); 200 | accessFile.setLength(zipBuffer.getEntriesDataSizeBytes()); 201 | accessFile.seek(zipBuffer.getEntriesDataSizeBytes()); 202 | if (padSizeBeforeApkSigningBlock != 0) 203 | accessFile.write(new byte[padSizeBeforeApkSigningBlock]); 204 | accessFile.write(result); 205 | int centralStart = (int) accessFile.getFilePointer(); 206 | centralDir.reset(); 207 | centralDir.copyTo(accessFile, centralDir.size()); 208 | byte[] eocdData = eocd.getBuffer(); 209 | int valueOffset = eocd.getStart() + 16; 210 | setInt(centralStart, eocdData, valueOffset); 211 | eocd.reset(); 212 | eocd.copyTo(accessFile, eocd.size()); 213 | } 214 | } 215 | 216 | private static byte[] concat(byte[]... sequence) { 217 | int payloadSize = 0; 218 | for (byte[] element : sequence) { 219 | payloadSize += element.length; 220 | } 221 | byte[] result = new byte[payloadSize]; 222 | int pos = 0; 223 | for (byte[] element : sequence) { 224 | System.arraycopy(element, 0, result, pos, element.length); 225 | pos += element.length; 226 | } 227 | return result; 228 | } 229 | 230 | private static byte[] encodeDigestPart(Collection algorithms) { 231 | List digests = new ArrayList<>(algorithms.size()); 232 | int length = 4; 233 | for (SignatureAlgorithm algorithm : algorithms) { 234 | byte[] data = encodeIdWithPrefixLengthData(algorithm.getId(), algorithm.getDigest()); 235 | length += data.length; 236 | digests.add(data); 237 | } 238 | byte[] result = new byte[length]; 239 | setInt(length - 4, result, 0); 240 | int pos = 4; 241 | for (byte[] digest : digests) { 242 | System.arraycopy(digest, 0, result, pos, digest.length); 243 | pos += digest.length; 244 | } 245 | return result; 246 | } 247 | 248 | private static byte[] encodeCertificatePart(X509Certificate... certificates) throws CertificateEncodingException { 249 | List encodes = new ArrayList<>(certificates.length); 250 | int length = 4; 251 | for (X509Certificate certificate : certificates) { 252 | byte[] data = certificate.getEncoded(); 253 | length += 4 + data.length; 254 | encodes.add(data); 255 | } 256 | byte[] result = new byte[length]; 257 | setInt(length - 4, result, 0); 258 | int pos = 4; 259 | for (byte[] encode : encodes) { 260 | setInt(encode.length, result, pos); 261 | pos += 4; 262 | System.arraycopy(encode, 0, result, pos, encode.length); 263 | pos += encode.length; 264 | } 265 | return result; 266 | } 267 | 268 | private static byte[] encodeSignature(Collection algorithms) { 269 | List digests = new ArrayList<>(algorithms.size()); 270 | int length = 0; 271 | for (SignatureAlgorithm algorithm : algorithms) { 272 | byte[] data = encodeIdWithPrefixLengthData(algorithm.getId(), algorithm.getSignature()); 273 | length += data.length; 274 | digests.add(data); 275 | } 276 | byte[] result = new byte[length]; 277 | int pos = 0; 278 | for (byte[] digest : digests) { 279 | System.arraycopy(digest, 0, result, pos, digest.length); 280 | pos += digest.length; 281 | } 282 | return result; 283 | } 284 | 285 | private static byte[] encodeAdditionalPart() { 286 | // length 0 287 | return new byte[4]; 288 | } 289 | 290 | private static byte[] encodeIdWithPrefixLengthData(int id, byte[] digest) { 291 | byte[] result = new byte[12 + digest.length]; 292 | setInt(digest.length + 8, result, 0); 293 | setInt(id, result, 4); 294 | setInt(digest.length, result, 8); 295 | System.arraycopy(digest, 0, result, 12, digest.length); 296 | return result; 297 | } 298 | 299 | private static int getPaddingSize(long length, int align) { 300 | int overCount = (int) (length % align); 301 | if (overCount == 0) 302 | return 0; 303 | return align - overCount; 304 | } 305 | 306 | private static List getSuggestedSignatureAlgorithms(PublicKey signingKey) throws InvalidKeyException { 307 | String keyAlgorithm = signingKey.getAlgorithm(); 308 | if ("RSA".equalsIgnoreCase(keyAlgorithm)) { 309 | int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength(); 310 | if (modulusLengthBits <= 3072) { 311 | return Arrays.asList( 312 | SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256(), 313 | SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256() 314 | ); 315 | } else { 316 | return Collections.singletonList( 317 | SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512() 318 | ); 319 | } 320 | } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { 321 | return Arrays.asList( 322 | SignatureAlgorithm.DSA_WITH_SHA256(), 323 | SignatureAlgorithm.VERITY_DSA_WITH_SHA256() 324 | ); 325 | } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { 326 | return Arrays.asList( 327 | SignatureAlgorithm.ECDSA_WITH_SHA256(), 328 | SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256() 329 | ); 330 | } else { 331 | throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm); 332 | } 333 | } 334 | 335 | private static byte[] encodePublicKey(PublicKey publicKey) throws InvalidKeyException, NoSuchAlgorithmException { 336 | byte[] encodedPublicKey; 337 | try { 338 | encodedPublicKey = 339 | KeyFactory.getInstance(publicKey.getAlgorithm()) 340 | .getKeySpec(publicKey, X509EncodedKeySpec.class) 341 | .getEncoded(); 342 | } catch (InvalidKeySpecException e) { 343 | throw new InvalidKeyException( 344 | "Failed to obtain X.509 encoded form of public key " + publicKey 345 | + " of class " + publicKey.getClass().getName(), 346 | e); 347 | } 348 | if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) { 349 | throw new InvalidKeyException( 350 | "Failed to obtain X.509 encoded form of public key " + publicKey 351 | + " of class " + publicKey.getClass().getName()); 352 | } 353 | return encodedPublicKey; 354 | } 355 | 356 | } 357 | -------------------------------------------------------------------------------- /src/bin/mt/apksign/VerityTreeBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bin.mt.apksign; 18 | 19 | import bin.mt.apksign.data.DataSink; 20 | import bin.mt.apksign.data.DataSinks; 21 | import bin.mt.apksign.data.DataSource; 22 | import bin.mt.apksign.data.DataSources; 23 | 24 | import java.io.IOException; 25 | import java.security.MessageDigest; 26 | import java.security.NoSuchAlgorithmException; 27 | import java.util.ArrayList; 28 | 29 | /** 30 | * VerityTreeBuilder is used to generate the root hash of verity tree built from the input file. 31 | * The root hash can be used on device for on-access verification. The tree itself is reproducible 32 | * on device, and is not shipped with the APK. 33 | */ 34 | class VerityTreeBuilder { 35 | 36 | /** 37 | * Maximum size (in bytes) of each node of the tree. 38 | */ 39 | private final static int CHUNK_SIZE = 4096; 40 | 41 | /** 42 | * Digest algorithm (JCA Digest algorithm name) used in the tree. 43 | */ 44 | private final static String JCA_ALGORITHM = "SHA-256"; 45 | 46 | /** 47 | * Optional salt to apply before each digestion. 48 | */ 49 | private final byte[] mSalt; 50 | 51 | private final MessageDigest mMd; 52 | 53 | VerityTreeBuilder(byte[] salt) throws NoSuchAlgorithmException { 54 | mSalt = salt; 55 | mMd = MessageDigest.getInstance(JCA_ALGORITHM); 56 | } 57 | 58 | /** 59 | * Returns the root hash of the APK verity tree built from ZIP blocks. 60 | *

61 | * Specifically, APK verity tree is built from the APK, but as if the APK Signing Block (which 62 | * must be page aligned) and the "Central Directory offset" field in End of Central Directory 63 | * are skipped. 64 | */ 65 | byte[] generateVerityTreeRootHash(DataSource beforeApkSigningBlock, DataSource centralDir, 66 | DataSource eocd) throws IOException { 67 | if (beforeApkSigningBlock.size() % CHUNK_SIZE != 0) { 68 | throw new IllegalStateException("APK Signing Block size not a multiple of " + CHUNK_SIZE 69 | + ": " + beforeApkSigningBlock.size()); 70 | } 71 | 72 | return generateVerityTreeRootHash(DataSources.link(beforeApkSigningBlock, centralDir, eocd)); 73 | } 74 | 75 | /** 76 | * Returns the root hash of the verity tree built from the data source. 77 | *

78 | * The tree is built bottom up. The bottom level has 256-bit digest for each 4 KB block in the 79 | * input file. If the total size is larger than 4 KB, take this level as input and repeat the 80 | * same procedure, until the level is within 4 KB. If salt is given, it will apply to each 81 | * digestion before the actual data. 82 | *

83 | * The returned root hash is calculated from the last level of 4 KB chunk, similarly with salt. 84 | *

85 | * The tree is currently stored only in memory and is never written out. Nevertheless, it is 86 | * the actual verity tree format on disk, and is supposed to be re-generated on device. 87 | *

88 | * This is package-private for testing purpose. 89 | */ 90 | private byte[] generateVerityTreeRootHash(DataSource fileSource) throws IOException { 91 | int digestSize = mMd.getDigestLength(); 92 | 93 | // Calculate the summed area table of level size. In other word, this is the offset 94 | // table of each level, plus the next non-existing level. 95 | int[] levelOffset = calculateLevelOffset(fileSource.size(), digestSize); 96 | 97 | 98 | byte[] verityBuffer = new byte[levelOffset[levelOffset.length - 1]]; 99 | 100 | // Generate the hash tree bottom-up. 101 | for (int i = levelOffset.length - 2; i >= 0; i--) { 102 | DataSink middleBufferSink = DataSinks.fromData(verityBuffer, levelOffset[i], levelOffset[i + 1]); 103 | DataSource src; 104 | if (i == levelOffset.length - 2) { 105 | src = fileSource; 106 | } else { 107 | int start = levelOffset[i + 1]; 108 | int end = levelOffset[i + 2]; 109 | src = DataSources.fromData(verityBuffer, start, end - start); 110 | } 111 | digestDataByChunks(src, middleBufferSink); 112 | 113 | // If the output is not full chunk, pad with 0s. 114 | long totalOutput = divideRoundup(src.size()) * digestSize; 115 | int incomplete = (int) (totalOutput % CHUNK_SIZE); 116 | if (incomplete > 0) { 117 | byte[] padding = new byte[CHUNK_SIZE - incomplete]; 118 | middleBufferSink.consume(padding, 0, padding.length); 119 | } 120 | } 121 | 122 | // Finally, calculate the root hash from the top level (only page). 123 | return saltedDigest(verityBuffer); 124 | } 125 | 126 | /** 127 | * Returns an array of summed area table of level size in the verity tree. In other words, the 128 | * returned array is offset of each level in the verity tree file format, plus an additional 129 | * offset of the next non-existing level (i.e. end of the last level + 1). Thus the array size 130 | * is level + 1. 131 | */ 132 | private static int[] calculateLevelOffset(long dataSize, int digestSize) { 133 | // Compute total size of each level, bottom to top. 134 | ArrayList levelSize = new ArrayList<>(); 135 | while (true) { 136 | long chunkCount = divideRoundup(dataSize); 137 | long size = CHUNK_SIZE * divideRoundup(chunkCount * digestSize); 138 | levelSize.add(size); 139 | if (chunkCount * digestSize <= CHUNK_SIZE) { 140 | break; 141 | } 142 | dataSize = chunkCount * digestSize; 143 | } 144 | 145 | // Reverse and convert to summed area table. 146 | int[] levelOffset = new int[levelSize.size() + 1]; 147 | levelOffset[0] = 0; 148 | for (int i = 0; i < levelSize.size(); i++) { 149 | // We don't support verity tree if it is larger then Integer.MAX_VALUE. 150 | levelOffset[i + 1] = levelOffset[i] + toIntExact( 151 | levelSize.get(levelSize.size() - i - 1)); 152 | } 153 | return levelOffset; 154 | } 155 | 156 | /** 157 | * Digest data source by chunks then feeds them to the sink one by one. If the last unit is 158 | * less than the chunk size and padding is desired, feed with extra padding 0 to fill up the 159 | * chunk before digesting. 160 | */ 161 | private void digestDataByChunks(DataSource dataSource, DataSink dataSink) throws IOException { 162 | dataSource = dataSource.align(CHUNK_SIZE); 163 | long size = dataSource.size(); 164 | long offset = 0; 165 | for (; offset + CHUNK_SIZE <= size; offset += CHUNK_SIZE) { 166 | byte[] hash = saltedDigest(dataSource); 167 | dataSink.consume(hash, 0, hash.length); 168 | } 169 | 170 | // Send the last incomplete chunk with 0 padding to the sink at once. 171 | int remaining = (int) (size % CHUNK_SIZE); 172 | if (remaining > 0) { 173 | throw new IllegalStateException("Remaining: " + remaining); 174 | } 175 | } 176 | 177 | private byte[] saltedDigest(DataSource source) throws IOException { 178 | mMd.reset(); 179 | if (mSalt != null) { 180 | mMd.update(mSalt); 181 | } 182 | source.copyTo(mMd, VerityTreeBuilder.CHUNK_SIZE); 183 | return mMd.digest(); 184 | } 185 | 186 | private byte[] saltedDigest(byte[] data) { 187 | mMd.reset(); 188 | if (mSalt != null) { 189 | mMd.update(mSalt); 190 | } 191 | mMd.update(data, 0, VerityTreeBuilder.CHUNK_SIZE); 192 | return mMd.digest(); 193 | } 194 | 195 | /** 196 | * Divides a number and round up to the closest integer. 197 | */ 198 | private static long divideRoundup(long dividend) { 199 | return (dividend + (long) VerityTreeBuilder.CHUNK_SIZE - 1) / (long) VerityTreeBuilder.CHUNK_SIZE; 200 | } 201 | 202 | private static int toIntExact(long value) { 203 | if ((int) value != value) { 204 | throw new ArithmeticException("integer overflow"); 205 | } 206 | return (int) value; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/bin/mt/apksign/ZipBuffer.java: -------------------------------------------------------------------------------- 1 | package bin.mt.apksign; 2 | 3 | import bin.io.RandomAccessFile; 4 | 5 | import java.io.EOFException; 6 | import java.io.IOException; 7 | 8 | class ZipBuffer { 9 | static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L; 10 | static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L; 11 | static final int EOCD_SIG = 0X06054B50; 12 | static final int MIN_EOCD_SIZE = 22; 13 | static final int MAX_EOCD_SIZE = MIN_EOCD_SIZE + 0xFFFF; 14 | static final int APK_SIG_BLOCK_MIN_SIZE = 32; 15 | 16 | private final long entriesDataSizeBytes; 17 | private final long centralDirectoryOffset; 18 | private final long centralDirectorySizeBytes; 19 | private final long eocdOffset; 20 | private final boolean hasApkSigBlock; 21 | private final RandomAccessFile file; 22 | 23 | ZipBuffer(RandomAccessFile file) throws IOException { 24 | this.file = file; 25 | boolean found = false; 26 | long length = length(); 27 | long off = length - MIN_EOCD_SIZE; 28 | final long stopSearching = 29 | Math.max(0L, length - MAX_EOCD_SIZE); 30 | while (off >= stopSearching) { 31 | seek(off); 32 | if (readInt() == EOCD_SIG) { 33 | found = true; 34 | break; 35 | } 36 | off--; 37 | } 38 | if (!found) { 39 | throw new IOException("Archive is not a ZIP archive"); 40 | } 41 | 42 | eocdOffset = off; 43 | // 没做zip64支持 44 | seek(off + 12); 45 | centralDirectorySizeBytes = readUInt(); 46 | centralDirectoryOffset = readUInt(); 47 | 48 | long entriesDataEnd = centralDirectoryOffset; 49 | boolean matchV2SigBlock = false; 50 | try { 51 | if (centralDirectoryOffset >= APK_SIG_BLOCK_MIN_SIZE) { 52 | seek(centralDirectoryOffset - 16); 53 | if (readLong() == APK_SIG_BLOCK_MAGIC_LO && readLong() == APK_SIG_BLOCK_MAGIC_HI) { 54 | seek(centralDirectoryOffset - 24); 55 | long size = readLong(); 56 | long sigStart = centralDirectoryOffset - size - 8; 57 | seek(sigStart); 58 | if (readLong() == size) { 59 | matchV2SigBlock = true; 60 | entriesDataEnd = sigStart; 61 | } 62 | } 63 | } 64 | } catch (Exception e) { 65 | e.printStackTrace(); 66 | } 67 | entriesDataSizeBytes = entriesDataEnd; 68 | hasApkSigBlock = matchV2SigBlock; 69 | } 70 | 71 | public long length() throws IOException { 72 | return file.length(); 73 | } 74 | 75 | public void seek(long position) throws IOException { 76 | file.seek(position); 77 | } 78 | 79 | public long position() throws IOException { 80 | return file.getFilePointer(); 81 | } 82 | 83 | public void skip(int length) throws IOException { 84 | if (length < 0) 85 | throw new IOException("Skip " + length); 86 | long pos = file.getFilePointer() + length; 87 | long len = file.length(); 88 | if (pos > len) 89 | throw new EOFException(); 90 | file.seek(pos); 91 | } 92 | 93 | public byte[] readBytes(int len) throws IOException { 94 | byte[] bytes = new byte[len]; 95 | file.readFully(bytes); 96 | return bytes; 97 | } 98 | 99 | public int readInt() throws IOException { 100 | int ch1 = file.read(); 101 | int ch2 = file.read(); 102 | int ch3 = file.read(); 103 | int ch4 = file.read(); 104 | if ((ch1 | ch2 | ch3 | ch4) < 0) 105 | throw new EOFException(); 106 | return (ch1) | (ch2 << 8) | (ch3 << 16) | (ch4 << 24); 107 | } 108 | 109 | public long readLong() throws IOException { 110 | long ch1 = file.read(); 111 | long ch2 = file.read(); 112 | long ch3 = file.read(); 113 | long ch4 = file.read(); 114 | long ch5 = file.read(); 115 | long ch6 = file.read(); 116 | long ch7 = file.read(); 117 | long ch8 = file.read(); 118 | if ((ch1 | ch2 | ch3 | ch4) < 0) 119 | throw new EOFException(); 120 | return (ch1) | (ch2 << 8) | (ch3 << 16) | (ch4 << 24) | (ch5 << 32) | (ch6 << 40) | (ch7 << 48) | (ch8 << 56); 121 | 122 | } 123 | 124 | public long readUInt() throws IOException { 125 | return readInt() & 0xFFFFFFFFL; 126 | } 127 | 128 | public long getEntriesDataSizeBytes() { 129 | return entriesDataSizeBytes; 130 | } 131 | 132 | public long getCentralDirectoryOffset() { 133 | return centralDirectoryOffset; 134 | } 135 | 136 | public long getCentralDirectorySizeBytes() { 137 | return centralDirectorySizeBytes; 138 | } 139 | 140 | public long getEocdOffset() { 141 | return eocdOffset; 142 | } 143 | 144 | public boolean hasApkSigBlock() { 145 | return hasApkSigBlock; 146 | } 147 | 148 | public RandomAccessFile getFile() { 149 | return file; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/bin/mt/apksign/data/ByteArrayDataSink.java: -------------------------------------------------------------------------------- 1 | package bin.mt.apksign.data; 2 | 3 | import java.io.EOFException; 4 | import java.io.IOException; 5 | 6 | public class ByteArrayDataSink implements DataSink { 7 | private byte[] data; 8 | private int pos; 9 | private int limit; 10 | 11 | ByteArrayDataSink(byte[] data, int pos, int limit) { 12 | this.data = data; 13 | this.pos = pos; 14 | this.limit = limit; 15 | } 16 | 17 | @Override 18 | public void consume(byte[] buf, int offset, int length) throws IOException { 19 | if (pos + length > limit) { 20 | throw new EOFException(); 21 | } 22 | System.arraycopy(buf, offset, data, pos, length); 23 | pos += length; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/bin/mt/apksign/data/ByteArrayDataSource.java: -------------------------------------------------------------------------------- 1 | package bin.mt.apksign.data; 2 | 3 | import java.io.EOFException; 4 | import java.io.IOException; 5 | import java.io.OutputStream; 6 | 7 | public class ByteArrayDataSource implements DataSource { 8 | private byte[] data; 9 | private int start; 10 | private int size; 11 | private int pos; 12 | 13 | ByteArrayDataSource(byte[] data, int start, int size) { 14 | if (start + size > data.length) 15 | throw new IllegalArgumentException(); 16 | this.data = data; 17 | this.start = start; 18 | this.size = size; 19 | this.pos = 0; 20 | } 21 | 22 | @Override 23 | public long size() { 24 | return size; 25 | } 26 | 27 | @Override 28 | public long pos() { 29 | return pos; 30 | } 31 | 32 | @Override 33 | public void reset() { 34 | pos = 0; 35 | } 36 | 37 | @Override 38 | public void copyTo(OutputStream os, long length) throws IOException { 39 | if (length > remaining()) 40 | throw new EOFException(); 41 | os.write(data, start + pos, (int) length); 42 | pos += length; 43 | } 44 | 45 | public byte[] getBuffer() { 46 | return data; 47 | } 48 | 49 | public int getStart() { 50 | return start; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/bin/mt/apksign/data/ChainedDataSource.java: -------------------------------------------------------------------------------- 1 | package bin.mt.apksign.data; 2 | 3 | import java.io.EOFException; 4 | import java.io.IOException; 5 | import java.io.OutputStream; 6 | 7 | public class ChainedDataSource implements DataSource { 8 | private DataSource[] sources; 9 | private DataSource currentSource; 10 | private int currentIndex; 11 | private long size; 12 | private long pos; 13 | 14 | ChainedDataSource(DataSource... sources) { 15 | if (sources.length == 0) 16 | throw new IllegalArgumentException(); 17 | this.sources = sources; 18 | this.currentIndex = 0; 19 | this.currentSource = sources[currentIndex]; 20 | this.pos = 0; 21 | this.size = 0; 22 | for (DataSource source : sources) { 23 | size += source.size(); 24 | } 25 | } 26 | 27 | @Override 28 | public long size() { 29 | return size; 30 | } 31 | 32 | @Override 33 | public long pos() { 34 | return pos; 35 | } 36 | 37 | @Override 38 | public void reset() throws IOException { 39 | this.currentIndex = 0; 40 | this.currentSource = sources[currentIndex]; 41 | this.pos = 0; 42 | for (DataSource source : sources) { 43 | source.reset(); 44 | } 45 | } 46 | 47 | @Override 48 | public void copyTo(OutputStream os, long length) throws IOException { 49 | if (length > remaining()) 50 | throw new EOFException(); 51 | while (length > 0) { 52 | long len = Math.min(length, currentSource.remaining()); 53 | currentSource.copyTo(os, len); 54 | length -= len; 55 | pos += len; 56 | if (currentSource.remaining() == 0 && currentIndex < sources.length - 1) { 57 | currentSource = sources[++currentIndex]; 58 | } 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/bin/mt/apksign/data/DataSink.java: -------------------------------------------------------------------------------- 1 | package bin.mt.apksign.data; 2 | 3 | import java.io.IOException; 4 | 5 | public interface DataSink { 6 | 7 | void consume(byte[] buf, int offset, int length) throws IOException; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/bin/mt/apksign/data/DataSinks.java: -------------------------------------------------------------------------------- 1 | package bin.mt.apksign.data; 2 | 3 | public class DataSinks { 4 | 5 | public static DataSink fromData(byte[] data) { 6 | return fromData(data, 0, data.length); 7 | } 8 | 9 | public static DataSink fromData(byte[] data, int position, int limit) { 10 | return new ByteArrayDataSink(data, position, limit); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/bin/mt/apksign/data/DataSource.java: -------------------------------------------------------------------------------- 1 | package bin.mt.apksign.data; 2 | 3 | import bin.io.RandomAccessFile; 4 | 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.IOException; 7 | import java.io.OutputStream; 8 | import java.security.MessageDigest; 9 | 10 | public interface DataSource { 11 | 12 | long size(); 13 | 14 | long pos(); 15 | 16 | default long remaining() { 17 | return size() - pos(); 18 | } 19 | 20 | void reset() throws IOException; 21 | 22 | void copyTo(OutputStream os, long length) throws IOException; 23 | 24 | default void copyTo(MessageDigest digest, long length) throws IOException { 25 | OutputStream os = new OutputStream() { 26 | @Override 27 | public void write(int b) { 28 | digest.update((byte) b); 29 | } 30 | 31 | @Override 32 | public void write(byte[] b, int off, int len) { 33 | digest.update(b, off, len); 34 | } 35 | }; 36 | copyTo(os, length); 37 | } 38 | 39 | default void copyTo(RandomAccessFile accessFile, long length) throws IOException { 40 | OutputStream os = new OutputStream() { 41 | @Override 42 | public void write(int b) throws IOException { 43 | accessFile.write(b); 44 | } 45 | 46 | @Override 47 | public void write(byte[] b, int off, int len) throws IOException { 48 | accessFile.write(b, off, len); 49 | } 50 | }; 51 | copyTo(os, length); 52 | } 53 | 54 | default DataSource align(int align) { 55 | return DataSources.align(this, align); 56 | } 57 | 58 | default ByteArrayDataSource toMemory() throws IOException { 59 | long remaining = remaining(); 60 | if (remaining > Integer.MAX_VALUE) { 61 | throw new IOException("Data too large"); 62 | } 63 | ByteArrayOutputStream baos = new ByteArrayOutputStream((int) remaining); 64 | copyTo(baos, remaining); 65 | return (ByteArrayDataSource) DataSources.fromData(baos.toByteArray()); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/bin/mt/apksign/data/DataSources.java: -------------------------------------------------------------------------------- 1 | package bin.mt.apksign.data; 2 | 3 | import bin.io.RandomAccessFile; 4 | 5 | import java.io.IOException; 6 | 7 | public class DataSources { 8 | 9 | public static DataSource fromFile(RandomAccessFile randomAccessFile, long start, long size) { 10 | return new FileDataSource(randomAccessFile, start, size); 11 | } 12 | 13 | public static DataSource fromData(byte[] data) { 14 | return fromData(data, 0, data.length); 15 | } 16 | 17 | public static DataSource fromData(byte[] data, int start, int size) { 18 | return new ByteArrayDataSource(data, start, size); 19 | } 20 | 21 | public static DataSource align(DataSource source, int align) { 22 | long size = source.size(); 23 | int overCount = (int) (size % align); 24 | if (overCount == 0) 25 | return source; 26 | int fillCount = align - overCount; 27 | return link(source, fromData(new byte[fillCount])); 28 | } 29 | 30 | public static DataSource link(DataSource... sources) { 31 | return new ChainedDataSource(sources); 32 | } 33 | 34 | public static void reset(DataSource... sources) throws IOException { 35 | for (DataSource source : sources) { 36 | source.reset(); 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/bin/mt/apksign/data/FileDataSource.java: -------------------------------------------------------------------------------- 1 | package bin.mt.apksign.data; 2 | 3 | import bin.io.RandomAccessFile; 4 | 5 | import java.io.EOFException; 6 | import java.io.IOException; 7 | import java.io.OutputStream; 8 | 9 | public class FileDataSource implements DataSource { 10 | private RandomAccessFile randomAccessFile; 11 | private long start; 12 | private long size; 13 | private long pos; 14 | 15 | FileDataSource(RandomAccessFile randomAccessFile, long start, long size) { 16 | this.randomAccessFile = randomAccessFile; 17 | this.start = start; 18 | this.size = size; 19 | } 20 | 21 | 22 | @Override 23 | public long size() { 24 | return size; 25 | } 26 | 27 | @Override 28 | public long pos() { 29 | return pos; 30 | } 31 | 32 | @Override 33 | public void reset() { 34 | pos = 0; 35 | } 36 | 37 | @Override 38 | public void copyTo(OutputStream os, long length) throws IOException { 39 | if (length > remaining()) 40 | throw new EOFException(); 41 | byte[] buf = new byte[4096]; 42 | int readLen; 43 | randomAccessFile.seek(start + pos); 44 | while (length > 0 && (readLen = randomAccessFile.read(buf, 0, (int) Math.min(length, buf.length))) != -1) { 45 | os.write(buf, 0, readLen); 46 | length -= readLen; 47 | pos += readLen; 48 | } 49 | if (length != 0) 50 | throw new IllegalStateException("Remaining length: " + length); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/bin/mt/apksign/key/JksSignatureKey.java: -------------------------------------------------------------------------------- 1 | package bin.mt.apksign.key; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.security.KeyStore; 6 | import java.security.PrivateKey; 7 | import java.security.cert.X509Certificate; 8 | 9 | public class JksSignatureKey implements SignatureKey { 10 | private X509Certificate certificate; 11 | private PrivateKey privateKey; 12 | 13 | public JksSignatureKey(String path, String storePassword, String alias, String aliasPassword) throws Exception { 14 | this(new File(path), storePassword, alias, aliasPassword); 15 | } 16 | 17 | public JksSignatureKey(File file, String storePassword, String alias, String aliasPassword) throws Exception { 18 | KeyStore keyStore = KeyStore.getInstance("jks"); 19 | keyStore.load(new FileInputStream(file), storePassword.toCharArray()); 20 | certificate = (X509Certificate) keyStore.getCertificate(alias); 21 | privateKey = (PrivateKey) keyStore.getKey(alias, aliasPassword.toCharArray()); 22 | } 23 | 24 | @Override 25 | public X509Certificate getCertificate() { 26 | return certificate; 27 | } 28 | 29 | @Override 30 | public PrivateKey getPrivateKey() { 31 | return privateKey; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/bin/mt/apksign/key/SignatureKey.java: -------------------------------------------------------------------------------- 1 | package bin.mt.apksign.key; 2 | 3 | import java.security.PrivateKey; 4 | import java.security.cert.X509Certificate; 5 | 6 | public interface SignatureKey { 7 | 8 | X509Certificate getCertificate() throws Exception; 9 | 10 | PrivateKey getPrivateKey() throws Exception; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/bin/zip/BridgeInputStream.java: -------------------------------------------------------------------------------- 1 | package bin.zip; 2 | 3 | import bin.io.RandomAccessFile; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | 8 | /** 9 | * @author Bin 10 | */ 11 | public class BridgeInputStream extends InputStream { 12 | private final RandomAccessFile archive; 13 | private long remaining; 14 | private long loc; 15 | 16 | public BridgeInputStream(RandomAccessFile archive, long start, long remaining) { 17 | this.archive = archive; 18 | this.remaining = remaining; 19 | loc = start; 20 | } 21 | 22 | public int read() throws IOException { 23 | if (remaining-- <= 0) { 24 | return -1; 25 | } 26 | synchronized (archive) { 27 | archive.seek(loc++); 28 | return archive.read(); 29 | } 30 | } 31 | 32 | @Override 33 | public int available() { 34 | return (int) (remaining & Integer.MAX_VALUE); 35 | } 36 | 37 | public int read(byte[] b, int off, int len) throws IOException { 38 | if (remaining <= 0) { 39 | return -1; 40 | } 41 | 42 | if (len <= 0) { 43 | return 0; 44 | } 45 | 46 | if (len > remaining) { 47 | len = (int) remaining; 48 | } 49 | int ret; 50 | synchronized (archive) { 51 | archive.seek(loc); 52 | ret = archive.read(b, off, len); 53 | } 54 | if (ret > 0) { 55 | loc += ret; 56 | remaining -= ret; 57 | } 58 | return ret; 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /src/bin/zip/BridgeOutputStream.java: -------------------------------------------------------------------------------- 1 | package bin.zip; 2 | 3 | import bin.io.RandomAccessFile; 4 | 5 | import java.io.IOException; 6 | import java.io.OutputStream; 7 | 8 | /** 9 | * @author Bin 10 | */ 11 | public class BridgeOutputStream extends OutputStream { 12 | private final RandomAccessFile archive; 13 | private long count = 0; 14 | 15 | public BridgeOutputStream(RandomAccessFile archive) { 16 | this.archive = archive; 17 | } 18 | 19 | @Override 20 | public void write(byte[] b, int off, int len) throws IOException { 21 | if (len > 0) { 22 | archive.write(b, off, len); 23 | count += len; 24 | } 25 | } 26 | 27 | @Override 28 | public void write(int b) throws IOException { 29 | archive.write(b); 30 | count++; 31 | } 32 | 33 | public long getCount() { 34 | return count; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/bin/zip/CenterFileHeader.java: -------------------------------------------------------------------------------- 1 | package bin.zip; 2 | 3 | /** 4 | * @author Bin 5 | */ 6 | class CenterFileHeader implements Comparable { 7 | int generalPurposeFlag; 8 | int method; 9 | int time; 10 | int crc; 11 | long compressedSize; 12 | long size; 13 | String nameStr; 14 | byte[] name; 15 | byte[] extra; 16 | byte[] comment; 17 | int diskNumberStart; 18 | int internalAttributes; 19 | int externalAttributes; 20 | long headerOffset; 21 | long dataOffset; 22 | boolean isDirectory; 23 | boolean isUtf8; 24 | boolean isHost; 25 | boolean sizeNeedZip64; 26 | boolean offsetNeedZip64; 27 | 28 | CenterFileHeader(String name) { 29 | this.nameStr = name; 30 | this.name = name.getBytes(ZipConstant.UTF_8); 31 | isUtf8 = true; 32 | extra = new byte[0]; 33 | comment = new byte[0]; 34 | time = (int) ZipUtil.javaToDosTime(System.currentTimeMillis()); 35 | isDirectory = name.endsWith("/") || name.endsWith("\\"); 36 | compressedSize = ZipEntry.UNKNOWN_SIZE; 37 | size = ZipEntry.UNKNOWN_SIZE; 38 | } 39 | 40 | CenterFileHeader(ZipEntry entry) { 41 | nameStr = entry.getName(); 42 | name = entry.getName().getBytes(ZipConstant.UTF_8); 43 | isUtf8 = true; 44 | time = (int) ZipUtil.javaToDosTime(entry.getTime()); 45 | method = entry.getMethod(); 46 | crc = entry.getCrc(); 47 | compressedSize = entry.getCompressedSize(); 48 | size = entry.getSize(); 49 | extra = entry.getExtra() == null ? new byte[0] : entry.getExtra(); 50 | comment = entry.getCommentData() == null ? new byte[0] : entry.getCommentData(); 51 | internalAttributes = entry.getInternalAttributes(); 52 | externalAttributes = entry.getExternalAttributes(); 53 | isDirectory = entry.isDirectory(); 54 | } 55 | 56 | boolean needZip64() { 57 | return sizeNeedZip64 || offsetNeedZip64; 58 | } 59 | 60 | boolean isEncrypted() { 61 | return (generalPurposeFlag & 1) != 0; 62 | } 63 | 64 | int version() { 65 | if (needZip64()) { 66 | return 45; 67 | } else if (method == ZipConstant.METHOD_STORED && !isEncrypted()) { 68 | return 10; 69 | } else { 70 | return 20; 71 | } 72 | } 73 | 74 | @Override 75 | public int compareTo(CenterFileHeader o) { 76 | return nameStr.compareTo(o.nameStr); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/bin/zip/CrcOutputStream.java: -------------------------------------------------------------------------------- 1 | package bin.zip; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | import java.util.zip.CRC32; 6 | 7 | /** 8 | * @author Bin 9 | */ 10 | public class CrcOutputStream extends OutputStream { 11 | private OutputStream os; 12 | private CRC32 crc32 = new CRC32(); 13 | private long count = 0; 14 | 15 | public CrcOutputStream(OutputStream os) { 16 | this.os = os; 17 | } 18 | 19 | @Override 20 | public void write(byte[] b, int off, int len) throws IOException { 21 | os.write(b, off, len); 22 | crc32.update(b, off, len); 23 | count += len; 24 | } 25 | 26 | @Override 27 | public void write(int b) throws IOException { 28 | os.write(b); 29 | crc32.update(b); 30 | count++; 31 | } 32 | 33 | public long getCount() { 34 | return count; 35 | } 36 | 37 | public int getCrc() { 38 | return (int) crc32.getValue(); 39 | } 40 | 41 | @Override 42 | public void close() throws IOException { 43 | os.close(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/bin/zip/DataMultiplexing.java: -------------------------------------------------------------------------------- 1 | package bin.zip; 2 | 3 | import bin.mt.apksign.V2V3SchemeSigner; 4 | import bin.mt.apksign.key.JksSignatureKey; 5 | 6 | import java.io.BufferedInputStream; 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.text.DecimalFormat; 11 | import java.util.*; 12 | 13 | public class DataMultiplexing { 14 | 15 | public static void main(String[] args) throws Exception { 16 | File input = new File("test.apk"); 17 | File output = new File("output.apk"); 18 | optimize(input, output, "assets/base.apk", true); 19 | V2V3SchemeSigner.sign(output, new JksSignatureKey("test.jks", "123456", "123456", "123456"), true, true); 20 | System.out.println("Check " + isZipFileContentEquals(input, output)); 21 | } 22 | 23 | /** 24 | * @param input 输入文件 25 | * @param output 输出文件 26 | * @param hostEntryName 原包路径,如 assets/base.apk 27 | * @param printDetails 是否打印优化详情 28 | */ 29 | public static void optimize(String input, String output, String hostEntryName, boolean printDetails) throws IOException { 30 | optimize(new File(input), new File(output), hostEntryName, printDetails); 31 | } 32 | 33 | /** 34 | * @param input 输入文件 35 | * @param output 输出文件 36 | * @param hostEntryName 原包路径,如 assets/base.apk 37 | * @param printDetails 是否打印优化详情 38 | */ 39 | public static void optimize(File input, File output, String hostEntryName, boolean printDetails) throws IOException { 40 | try (ZipFile zipFile = new ZipFile(input)) { 41 | ZipEntry hostEntry = zipFile.getEntryNonNull(hostEntryName); 42 | Set children = new TreeSet<>(); 43 | // 返回的innerZipFile已经close了,但内部的entries还在 44 | ZipFile innerZipFile = collectChildren(zipFile, hostEntry, children); 45 | if (innerZipFile == null) { 46 | throw new IOException("No multiplexable data found"); 47 | } 48 | List otherZipEntry = new ArrayList<>(); 49 | for (ZipEntry entry : zipFile.getEntries()) { 50 | if (entry != hostEntry && !children.contains(entry.getName())) { 51 | otherZipEntry.add(entry); 52 | } 53 | } 54 | try (ZipMaker zipMaker = new ZipMaker(output)) { 55 | ZipMaker.HostEntryHolder holder = zipMaker.putNextHostEntry(hostEntry.getName(), innerZipFile); 56 | String format = "%0" + Math.min(Long.toHexString(hostEntry.getSize()).length(), 9) + "x"; 57 | if (printDetails) { 58 | System.out.println(hostEntry.getName() + " >> offset=0x" + Long.toHexString(holder.getHostEntryHeaderOffset())); 59 | } 60 | for (String name : children) { 61 | long offset = holder.putNextVirtualEntry(name); 62 | if (printDetails) { 63 | System.out.println(" +0x" + String.format(format, offset) + " " + name); 64 | } 65 | } 66 | for (ZipEntry entry : otherZipEntry) { 67 | zipMaker.copyZipEntry(entry, zipFile); 68 | } 69 | } 70 | } 71 | long inputLen = input.length(); 72 | long outputLen = output.length(); 73 | System.out.printf("Data multiplexing optimize: %s (%s) -> %s (%s) [%.2f%%]\n", input.getName(), formatFileSize(inputLen), output.getName(), formatFileSize(outputLen), (outputLen - inputLen) * 100f / inputLen); 74 | } 75 | 76 | /** 77 | * 判断两个ZIP文件内容是否完全相同 78 | */ 79 | public static boolean isZipFileContentEquals(File file1, File file2) throws IOException { 80 | try (ZipFile zipFile1 = new ZipFile(file1); ZipFile zipFile2 = new ZipFile(file2)) { 81 | if (zipFile1.getEntrySize() != zipFile2.getEntrySize()) { 82 | return false; 83 | } 84 | for (ZipEntry entry1 : zipFile1.getEntries()) { 85 | ZipEntry entry2 = zipFile2.getEntry(entry1.getName()); 86 | if (entry2 == null) { 87 | return false; 88 | } 89 | if (entry1.isDirectory() && entry2.isDirectory()) { 90 | continue; 91 | } 92 | if (entry1.getMethod() != entry2.getMethod()) { 93 | return false; 94 | } 95 | if (entry1.getCrc() != entry2.getCrc()) { 96 | return false; 97 | } 98 | if (entry1.getSize() != entry2.getSize()) { 99 | return false; 100 | } 101 | if (!Arrays.equals(entry1.getCommentData(), entry2.getCommentData())) { 102 | return false; 103 | } 104 | if (!isInputStreamContentEquals(zipFile1.getInputStream(entry1), zipFile2.getInputStream(entry2))) { 105 | return false; 106 | } 107 | } 108 | return true; 109 | } 110 | } 111 | 112 | private static ZipFile collectChildren(ZipFile outer, ZipEntry hostEntry, Set children) throws IOException { 113 | try (ZipFile inner = openEntryAsZipFile(outer, hostEntry)) { 114 | for (ZipEntry outerEntry : outer.getEntries()) { 115 | if (outerEntry == hostEntry || outerEntry.isDirectory()) { 116 | continue; 117 | } 118 | ZipEntry innerEntry = inner.getEntry(outerEntry.getName()); 119 | if (innerEntry == null) { 120 | continue; 121 | } 122 | if (outerEntry.getMethod() != innerEntry.getMethod()) { 123 | continue; 124 | } 125 | if (outerEntry.getCrc() != innerEntry.getCrc()) { 126 | continue; 127 | } 128 | if (outerEntry.getSize() != innerEntry.getSize()) { 129 | continue; 130 | } 131 | if (!Arrays.equals(outerEntry.getCommentData(), innerEntry.getCommentData())) { 132 | continue; 133 | } 134 | // 必须4k对齐 135 | if (innerEntry.getMethod() == ZipMaker.METHOD_STORED) { 136 | String name = innerEntry.getName(); 137 | if (name.equals("resources.arsc") && innerEntry.getDataOffset() % 4 != 0) { 138 | continue; 139 | } 140 | if (name.endsWith(".so") && innerEntry.getDataOffset() % 4096 != 0) { 141 | continue; 142 | } 143 | } 144 | boolean equals = outerEntry.getCompressedSize() == innerEntry.getCompressedSize() && 145 | isInputStreamContentEquals(inner.getRawInputStream(innerEntry), outer.getRawInputStream(outerEntry)) || 146 | isInputStreamContentEquals(inner.getInputStream(innerEntry), outer.getInputStream(outerEntry)); 147 | if (equals) { 148 | children.add(innerEntry.getName()); 149 | } 150 | } 151 | return children.isEmpty() ? null : inner; 152 | } 153 | } 154 | 155 | private static ZipFile openEntryAsZipFile(ZipFile zipFile, ZipEntry hostEntry) throws IOException { 156 | if (hostEntry.getMethod() == ZipMaker.METHOD_STORED) { 157 | return zipFile.openEntryAsZipFile(hostEntry); 158 | } else { 159 | throw new IOException("Entry must be packaged with the stored method: " + hostEntry.getName()); 160 | } 161 | } 162 | 163 | private static boolean isInputStreamContentEquals(InputStream input1, InputStream input2) throws IOException { 164 | if (input1 == input2) { 165 | return true; 166 | } 167 | if (!(input1 instanceof BufferedInputStream)) { 168 | input1 = new BufferedInputStream(input1); 169 | } 170 | if (!(input2 instanceof BufferedInputStream)) { 171 | input2 = new BufferedInputStream(input2); 172 | } 173 | 174 | int ch = input1.read(); 175 | while (-1 != ch) { 176 | final int ch2 = input2.read(); 177 | if (ch != ch2) { 178 | return false; 179 | } 180 | ch = input1.read(); 181 | } 182 | 183 | final int ch2 = input2.read(); 184 | return ch2 == -1; 185 | } 186 | 187 | private static final DecimalFormat df = new DecimalFormat("#.00"); 188 | 189 | private static String formatFileSize(long fileSize) { 190 | if (fileSize < 1024) 191 | return fileSize + "B"; 192 | else if (fileSize < 1024 * 1024) 193 | return df.format((double) fileSize / 1024) + "KB"; 194 | else if (fileSize < 1024 * 1024 * 1024) 195 | return df.format((double) fileSize / 1048576) + "MB"; 196 | else 197 | return df.format((double) fileSize / 1073741824) + "GB"; 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /src/bin/zip/ExtraDataRecord.java: -------------------------------------------------------------------------------- 1 | package bin.zip; 2 | 3 | import java.io.IOException; 4 | import java.util.Arrays; 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | 8 | /** 9 | * @author Bin 10 | */ 11 | class ExtraDataRecord { 12 | private static final Set KNOWN_HEADER = new HashSet<>(); 13 | 14 | static { 15 | // https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT 16 | // 4.5.2 The current Header ID mappings defined by PKWARE are: 17 | KNOWN_HEADER.add(0x0001); 18 | KNOWN_HEADER.add(0x0007); 19 | KNOWN_HEADER.add(0x0008); 20 | KNOWN_HEADER.add(0x0009); 21 | KNOWN_HEADER.add(0x000a); 22 | KNOWN_HEADER.add(0x000c); 23 | KNOWN_HEADER.add(0x000d); 24 | KNOWN_HEADER.add(0x000e); 25 | KNOWN_HEADER.add(0x000f); 26 | KNOWN_HEADER.add(0x0014); 27 | KNOWN_HEADER.add(0x0015); 28 | KNOWN_HEADER.add(0x0016); 29 | KNOWN_HEADER.add(0x0017); 30 | KNOWN_HEADER.add(0x0018); 31 | KNOWN_HEADER.add(0x0019); 32 | KNOWN_HEADER.add(0x0020); 33 | KNOWN_HEADER.add(0x0021); 34 | KNOWN_HEADER.add(0x0022); 35 | KNOWN_HEADER.add(0x0023); 36 | KNOWN_HEADER.add(0x0065); 37 | KNOWN_HEADER.add(0x0066); 38 | KNOWN_HEADER.add(0x4690); 39 | KNOWN_HEADER.add(0x07c8); 40 | KNOWN_HEADER.add(0x2605); 41 | KNOWN_HEADER.add(0x2705); 42 | KNOWN_HEADER.add(0x2805); 43 | KNOWN_HEADER.add(0x334d); 44 | KNOWN_HEADER.add(0x4341); 45 | KNOWN_HEADER.add(0x4453); 46 | KNOWN_HEADER.add(0x4704); 47 | KNOWN_HEADER.add(0x470f); 48 | KNOWN_HEADER.add(0x4b46); 49 | KNOWN_HEADER.add(0x4c41); 50 | KNOWN_HEADER.add(0x4d49); 51 | KNOWN_HEADER.add(0x4f4c); 52 | KNOWN_HEADER.add(0x5356); 53 | KNOWN_HEADER.add(0x5455); 54 | KNOWN_HEADER.add(0x554e); 55 | KNOWN_HEADER.add(0x5855); 56 | KNOWN_HEADER.add(0x6375); 57 | KNOWN_HEADER.add(0x6542); 58 | KNOWN_HEADER.add(0x7075); 59 | KNOWN_HEADER.add(0x756e); 60 | KNOWN_HEADER.add(0x7855); 61 | KNOWN_HEADER.add(0xa11e); 62 | KNOWN_HEADER.add(0xa220); 63 | KNOWN_HEADER.add(0xfd4a); 64 | KNOWN_HEADER.add(0x9901); 65 | KNOWN_HEADER.add(0x9902); 66 | } 67 | 68 | /** 69 | * 去除无效数据 70 | */ 71 | public static byte[] trim(byte[] extra) throws IOException { 72 | int offset = 0; 73 | while (extra.length - offset >= 4) { 74 | int header = ZipUtil.readUShort(extra, offset); 75 | int size = ZipUtil.readUShort(extra, offset + 2); 76 | if (!KNOWN_HEADER.contains(header) || offset + 4 + size > extra.length) { 77 | break; 78 | } 79 | offset += 4 + size; 80 | } 81 | return Arrays.copyOf(extra, offset); 82 | } 83 | 84 | public static byte[] set(byte[] extra, int header, byte[] data) throws IOException { 85 | extra = remove(extra, header); 86 | byte[] newExtra = new byte[4 + data.length + extra.length]; 87 | ZipUtil.writeShort(newExtra, 0, header); 88 | ZipUtil.writeShort(newExtra, 2, data.length); 89 | System.arraycopy(data, 0, newExtra, 4, data.length); 90 | System.arraycopy(extra, 0, newExtra, 4 + data.length, extra.length); 91 | return newExtra; 92 | } 93 | 94 | public static byte[] remove(byte[] extra, int header) throws IOException { 95 | int offset = 0; 96 | while (extra.length - offset >= 4) { 97 | int h = ZipUtil.readUShort(extra, offset); 98 | int size = ZipUtil.readUShort(extra, offset + 2); 99 | offset += 4; 100 | if (size > extra.length - offset) 101 | return extra; 102 | if (h != header) { 103 | offset += size; 104 | } else { 105 | offset -= 4; 106 | size += 4; 107 | byte[] bytes = new byte[extra.length - size]; 108 | System.arraycopy(extra, 0, bytes, 0, offset); 109 | System.arraycopy(extra, offset + size, bytes, offset, extra.length - size - offset); 110 | return bytes; 111 | } 112 | } 113 | return extra; 114 | } 115 | 116 | public static ExtraDataRecord find(byte[] extra, int header) throws IOException { 117 | int offset = 0; 118 | while (extra.length - offset >= 4) { 119 | int h = ZipUtil.readUShort(extra, offset); 120 | int size = ZipUtil.readUShort(extra, offset + 2); 121 | offset += 4; 122 | if (size > extra.length - offset) 123 | return null; 124 | if (h != header) { 125 | offset += size; 126 | } else { 127 | byte[] bytes = new byte[size]; 128 | System.arraycopy(extra, offset, bytes, 0, size); 129 | ExtraDataRecord record = new ExtraDataRecord(); 130 | record.setHeader(header); 131 | record.setSizeOfData(size); 132 | record.setData(bytes); 133 | return record; 134 | } 135 | } 136 | return null; 137 | } 138 | 139 | public static byte[] generateAESExtra(int aesKeyStrength, int method) throws IOException { 140 | int versionNumber = 2; // 2 141 | String vendorID = "AE"; // 2 142 | // aesKeyStrength // 1 143 | // method // 2 144 | byte[] data = new byte[7]; 145 | ZipUtil.writeShort(data, 0, versionNumber); 146 | ZipUtil.writeBytes(data, 2, vendorID.getBytes()); 147 | ZipUtil.writeByte(data, 4, aesKeyStrength); 148 | ZipUtil.writeShort(data, 5, method); 149 | return data; 150 | } 151 | 152 | private int header; 153 | 154 | private int sizeOfData; 155 | 156 | private byte[] data; 157 | 158 | public int getHeader() { 159 | return header; 160 | } 161 | 162 | public void setHeader(int header) { 163 | this.header = header; 164 | } 165 | 166 | public int getSizeOfData() { 167 | return sizeOfData; 168 | } 169 | 170 | public void setSizeOfData(int sizeOfData) { 171 | this.sizeOfData = sizeOfData; 172 | } 173 | 174 | public byte[] getData() { 175 | return data; 176 | } 177 | 178 | public void setData(byte[] data) { 179 | this.data = data; 180 | } 181 | 182 | public int readUByte(int off) throws IOException { 183 | return ZipUtil.readUByte(data, off); 184 | } 185 | 186 | public int readUShort(int off) throws IOException { 187 | return ZipUtil.readUShort(data, off); 188 | } 189 | 190 | public int readInt(int off) throws IOException { 191 | return ZipUtil.readInt(data, off); 192 | } 193 | 194 | public long readLong(int off) throws IOException { 195 | return ZipUtil.readLong(data, off); 196 | } 197 | 198 | } 199 | -------------------------------------------------------------------------------- /src/bin/zip/NoWrapDeflaterOutputStream.java: -------------------------------------------------------------------------------- 1 | package bin.zip; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | import java.util.zip.Deflater; 6 | import java.util.zip.DeflaterOutputStream; 7 | 8 | /** 9 | * @author Bin 10 | */ 11 | public class NoWrapDeflaterOutputStream extends DeflaterOutputStream { 12 | 13 | public NoWrapDeflaterOutputStream(OutputStream os, int level) { 14 | super(os, new Deflater(level, true)); 15 | } 16 | 17 | @Override 18 | public void close() throws IOException { 19 | super.close(); 20 | def.end(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/bin/zip/NoWrapInflaterInputStream.java: -------------------------------------------------------------------------------- 1 | package bin.zip; 2 | 3 | import java.io.EOFException; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.util.zip.Inflater; 7 | import java.util.zip.InflaterInputStream; 8 | import java.util.zip.ZipException; 9 | 10 | /** 11 | * @author Bin 12 | */ 13 | public class NoWrapInflaterInputStream extends InflaterInputStream { 14 | private final ZipEntry entry; 15 | 16 | public NoWrapInflaterInputStream(ZipEntry entry, InputStream in) { 17 | super(in, new Inflater(true)); 18 | this.entry = entry; 19 | } 20 | 21 | @Override 22 | public int read(byte[] b, int off, int len) throws IOException { 23 | try { 24 | return super.read(b, off, len); 25 | } catch (ZipException e) { 26 | e.printStackTrace(); 27 | throw new ZipException("Error: " + e.getMessage() + " (" + entry.getName() + ")"); 28 | } catch (EOFException e) { 29 | return -1; 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/bin/zip/ZipConstant.java: -------------------------------------------------------------------------------- 1 | package bin.zip; 2 | 3 | import java.nio.charset.Charset; 4 | 5 | /** 6 | * @author Bin 7 | */ 8 | interface ZipConstant { 9 | @SuppressWarnings("CharsetObjectCanBeUsed") 10 | Charset UTF_8 = Charset.forName("UTF-8"); 11 | int METHOD_STORED = 0; 12 | int METHOD_DEFLATED = 8; 13 | 14 | int PLATFORM_FAT = 0; 15 | 16 | int UFT8_NAMES_FLAG = 1 << 11; 17 | 18 | int EXTRA_HEADER_UNICODE_NAME = 0x7075; 19 | int EXTRA_HEADER_UNICODE_COMMENT = 0x6375; 20 | 21 | /** 22 | * local file header signature 23 | */ 24 | int LFH_SIG = 0x04034B50; 25 | 26 | /** 27 | * local file data descriptor signature 28 | */ 29 | int EXT_SIG = 0x08074b50; 30 | 31 | /** 32 | * End of central dir signature 33 | */ 34 | int EOCD_SIG = 0x06054B50; 35 | 36 | /** 37 | * Central file header signature 38 | */ 39 | int CFH_SIG = 0x02014B50; 40 | 41 | int BUFF_SIZE = 1024 * 4; 42 | 43 | int SHORT = 2; 44 | 45 | int WORD = 4; 46 | 47 | int MIN_EOCD_SIZE = 48 | /* end of central dir signature */ WORD 49 | /* number of this disk */ + SHORT 50 | /* number of the disk with the */ 51 | /* start of the central directory */ + SHORT 52 | /* total number of entries in */ 53 | /* the central dir on this disk */ + SHORT 54 | /* total number of entries in */ 55 | /* the central dir */ + SHORT 56 | /* size of the central directory */ + WORD 57 | /* offset of start of central */ 58 | /* directory with respect to */ 59 | /* the starting disk number */ + WORD 60 | /* zipfile comment length */ + SHORT; 61 | 62 | int MAX_EOCD_SIZE = MIN_EOCD_SIZE 63 | /* maximum length of zipfile comment */ + 0xFFFF; 64 | 65 | int CFD_LOCATOR_OFFSET = 66 | /* end of central dir signature */ WORD 67 | /* number of this disk */ + SHORT 68 | /* number of the disk with the */ 69 | /* start of the central directory */ + SHORT 70 | /* total number of entries in */ 71 | /* the central dir on this disk */ + SHORT 72 | /* total number of entries in */ 73 | /* the central dir */ + SHORT 74 | /* size of the central directory */ + WORD; 75 | 76 | int LFH_OFFSET_FOR_FILENAME_LENGTH = 77 | /* local file header signature */ WORD 78 | /* version needed to extract */ + SHORT 79 | /* general purpose bit flag */ + SHORT 80 | /* compression method */ + SHORT 81 | /* last mod file time */ + SHORT 82 | /* last mod file date */ + SHORT 83 | /* crc-32 */ + WORD 84 | /* compressed size */ + WORD 85 | /* uncompressed size */ + WORD; 86 | 87 | /** 88 | * The maximum supported entry / archive size for standard (non zip64) entries and archives. 89 | */ 90 | long MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE = 0x00000000ffffffffL; 91 | 92 | /* 93 | * Size (in bytes) of the zip64 end of central directory locator. This will be located 94 | * immediately before the end of central directory record if a given zipfile is in the 95 | * zip64 format. 96 | */ 97 | int ZIP64_LOCATOR_SIZE = 20; 98 | 99 | /** 100 | * The zip64 end of central directory locator signature (4 bytes wide). 101 | */ 102 | int ZIP64_LOCATOR_SIGNATURE = 0x07064b50; 103 | 104 | /** 105 | * The zip64 end of central directory record singature (4 bytes wide). 106 | */ 107 | int ZIP64_EOCD_RECORD_SIGNATURE = 0x06064b50; 108 | 109 | /** 110 | * The header ID of the zip64 extended info header. This value is used to identify 111 | * zip64 data in the "extra" field in the file headers. 112 | */ 113 | short ZIP64_EXTENDED_INFO_HEADER_ID = 0x0001; 114 | 115 | /** 116 | * The "effective" size of the zip64 eocd record. This excludes the fields that 117 | * are proprietary, signature, or fields we aren't interested in. We include the 118 | * following (contiguous) fields in this calculation : 119 | * - disk number (4 bytes) 120 | * - disk with start of central directory (4 bytes) 121 | * - number of central directory entries on this disk (8 bytes) 122 | * - total number of central directory entries (8 bytes) 123 | * - size of the central directory (8 bytes) 124 | * - offset of the start of the central directory (8 bytes) 125 | */ 126 | int ZIP64_EOCD_RECORD_EFFECTIVE_SIZE = 40; 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/bin/zip/ZipEntry.java: -------------------------------------------------------------------------------- 1 | package bin.zip; 2 | 3 | import java.io.IOException; 4 | 5 | import static bin.zip.ZipConstant.MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE; 6 | import static bin.zip.ZipConstant.ZIP64_EXTENDED_INFO_HEADER_ID; 7 | 8 | /** 9 | * @author Bin 10 | */ 11 | public class ZipEntry { 12 | public static final long UNKNOWN_SIZE = -1; 13 | private int platform = ZipConstant.PLATFORM_FAT; 14 | private int generalPurposeFlag; 15 | private int method; 16 | private String name; 17 | private long time; 18 | private int crc; 19 | private long compressedSize = UNKNOWN_SIZE; 20 | private long size = UNKNOWN_SIZE; 21 | private int internalAttributes = 0; 22 | private int externalAttributes = 0; 23 | private long headerOffset; 24 | private long dataOffset; 25 | private byte[] extra; 26 | private byte[] commentData; 27 | 28 | ZipEntry() { 29 | } 30 | 31 | public ZipEntry(String name) { 32 | setName(name); 33 | } 34 | 35 | public int getPlatform() { 36 | return platform; 37 | } 38 | 39 | void setPlatform(int platform) { 40 | this.platform = platform; 41 | } 42 | 43 | public int getGeneralPurposeFlag() { 44 | return generalPurposeFlag; 45 | } 46 | 47 | void setGeneralPurposeFlag(int generalPurposeFlag) { 48 | this.generalPurposeFlag = generalPurposeFlag; 49 | } 50 | 51 | public int getMethod() { 52 | return method; 53 | } 54 | 55 | public void setMethod(int method) { 56 | this.method = method; 57 | } 58 | 59 | public long getTime() { 60 | return time; 61 | } 62 | 63 | public void setTime(long time) { 64 | this.time = time; 65 | } 66 | 67 | public int getCrc() { 68 | return crc; 69 | } 70 | 71 | void setCrc(int crc) { 72 | this.crc = crc; 73 | } 74 | 75 | public long getCompressedSize() { 76 | return compressedSize; 77 | } 78 | 79 | public void setCompressedSize(long compressedSize) { 80 | this.compressedSize = compressedSize; 81 | } 82 | 83 | public long getSize() { 84 | return size; 85 | } 86 | 87 | public void setSize(long size) { 88 | this.size = size; 89 | } 90 | 91 | public boolean isDirectory() { 92 | return getName().endsWith("/"); 93 | } 94 | 95 | public String getName() { 96 | return name; 97 | } 98 | 99 | public void setName(String name) { 100 | if (name == null) 101 | name = ""; 102 | if (getPlatform() == ZipConstant.PLATFORM_FAT && !name.contains("/")) { 103 | name = name.replace('\\', '/'); 104 | } 105 | this.name = name; 106 | } 107 | 108 | public byte[] getExtra() { 109 | return extra; 110 | } 111 | 112 | void setExtra(byte[] extra) { 113 | this.extra = extra; 114 | } 115 | 116 | public int getInternalAttributes() { 117 | return internalAttributes; 118 | } 119 | 120 | void setInternalAttributes(int internalAttributes) { 121 | this.internalAttributes = internalAttributes; 122 | } 123 | 124 | public int getExternalAttributes() { 125 | return externalAttributes; 126 | } 127 | 128 | void setExternalAttributes(int externalAttributes) { 129 | this.externalAttributes = externalAttributes; 130 | } 131 | 132 | public long getHeaderOffset() { 133 | return headerOffset; 134 | } 135 | 136 | void setHeaderOffset(long headerOffset) { 137 | this.headerOffset = headerOffset; 138 | } 139 | 140 | public long getDataOffset() { 141 | return dataOffset; 142 | } 143 | 144 | void setDataOffset(long dataOffset) { 145 | this.dataOffset = dataOffset; 146 | } 147 | 148 | boolean setupZip64WithCenterDirectoryExtra(byte[] extra) throws IOException { 149 | ExtraDataRecord record = ExtraDataRecord.find(extra, ZIP64_EXTENDED_INFO_HEADER_ID); 150 | if (record == null) { 151 | if (compressedSize == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE || 152 | size == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE || 153 | headerOffset == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) { 154 | throw new IOException("File contains no zip64 extended information: " 155 | + "name=" + name + ", compressedSize=" + compressedSize + ", size=" 156 | + size + ", headerOffset=" + headerOffset); 157 | } 158 | return false; 159 | } 160 | int offset = 0; 161 | if (size == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) { 162 | size = record.readLong(offset); 163 | offset += 8; 164 | } 165 | if (compressedSize == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) { 166 | compressedSize = record.readLong(offset); 167 | offset += 8; 168 | } 169 | if (headerOffset == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) { 170 | headerOffset = record.readLong(offset); 171 | } 172 | return true; 173 | } 174 | 175 | void setNameData(byte[] nameData) { 176 | this.name = new String(nameData, ZipConstant.UTF_8); 177 | } 178 | 179 | void setCommentData(byte[] commentData) { 180 | this.commentData = commentData; 181 | } 182 | 183 | public byte[] getCommentData() { 184 | return commentData; 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /src/bin/zip/ZipFile.java: -------------------------------------------------------------------------------- 1 | package bin.zip; 2 | 3 | import bin.io.RandomAccessFactory; 4 | import bin.io.RandomAccessFile; 5 | 6 | import java.io.*; 7 | import java.util.*; 8 | 9 | import static bin.zip.ZipConstant.*; 10 | 11 | /** 12 | * @author Bin 13 | */ 14 | public class ZipFile implements Closeable { 15 | private final RandomAccessFile archive; 16 | private final Map entries = new LinkedHashMap<>(); 17 | 18 | public ZipFile(File file) throws IOException { 19 | this(RandomAccessFactory.from(file, "r")); 20 | } 21 | 22 | public ZipFile(RandomAccessFile archive) throws IOException { 23 | this.archive = archive; 24 | readEntries(); 25 | } 26 | 27 | public ZipEntry getEntry(String name) { 28 | return entries.get(name); 29 | } 30 | 31 | public ZipEntry getEntryNonNull(String name) throws IOException { 32 | ZipEntry entry = entries.get(name); 33 | if (entry == null) { 34 | throw new IOException("Entry not found: " + name); 35 | } 36 | return entry; 37 | } 38 | 39 | public ArrayList getEntries() { 40 | return new ArrayList<>(entries.values()); 41 | } 42 | 43 | public int getEntrySize() { 44 | return entries.size(); 45 | } 46 | 47 | private void readEntries() throws IOException { 48 | EocdRecord eocdRecord = readEocdRecord(); 49 | if (eocdRecord == null) { 50 | throw new IOException("EOCD not found"); 51 | } 52 | List list = new ArrayList<>(); 53 | boolean zip64 = eocdRecord.zip64; 54 | _seek(eocdRecord.centralDirOffset); 55 | while (_readInt() == CFH_SIG) { 56 | ZipEntry ze = new ZipEntry(); 57 | int versionMadeBy = _readUShort(); 58 | ze.setPlatform((versionMadeBy >> 8) & 0xF); 59 | 60 | _readUShort(); // skip version info 61 | 62 | ze.setGeneralPurposeFlag(_readUShort()); 63 | ze.setMethod(_readUShort()); 64 | ze.setTime(ZipUtil.dosToJavaTime(_readUInt())); 65 | ze.setCrc(_readInt()); 66 | 67 | ze.setCompressedSize(_readUInt()); 68 | ze.setSize(_readUInt()); 69 | 70 | int fileNameLen = _readUShort(); 71 | int extraLen = _readUShort(); 72 | int commentLen = _readUShort(); 73 | 74 | _readUShort(); // disk number 75 | 76 | ze.setInternalAttributes(_readUShort()); 77 | ze.setExternalAttributes(_readInt()); 78 | 79 | ze.setHeaderOffset(_readUInt()); 80 | 81 | ze.setNameData(_readBytes(fileNameLen)); 82 | 83 | if (extraLen > 0) { 84 | if (zip64) 85 | ze.setupZip64WithCenterDirectoryExtra(_readBytes(extraLen)); 86 | else 87 | _skip(extraLen); 88 | } 89 | 90 | if (commentLen > 0) { 91 | try { 92 | byte[] comment = _readBytes(commentLen); 93 | ze.setCommentData(comment); 94 | } catch (IOException ignored) { 95 | } 96 | } 97 | 98 | list.add(ze); 99 | } 100 | 101 | //noinspection Java8ListSort,ComparatorCombinators 102 | Collections.sort(list, (e1, e2) -> Long.compare(e1.getHeaderOffset(), e2.getHeaderOffset())); 103 | Set ok = new HashSet<>(list.size()); 104 | 105 | for (ZipEntry entry : list) { 106 | try { 107 | long offset = entry.getHeaderOffset(); 108 | _seek(offset + LFH_OFFSET_FOR_FILENAME_LENGTH); 109 | int fileNameLen = _readUShort(); 110 | int extraLen = _readUShort(); 111 | _skip(fileNameLen); 112 | byte[] extra = _readBytes(extraLen); 113 | // 去除zip64Extra 114 | extra = ExtraDataRecord.remove(extra, ZIP64_EXTENDED_INFO_HEADER_ID); 115 | entry.setExtra(extra); 116 | entry.setDataOffset(offset + LFH_OFFSET_FOR_FILENAME_LENGTH 117 | + SHORT + SHORT + fileNameLen + extraLen); 118 | ok.add(entry.getName()); 119 | } catch (EOFException e) { 120 | e.printStackTrace(); 121 | } 122 | } 123 | entries.clear(); 124 | for (ZipEntry entry : list) { 125 | String key = entry.getName(); 126 | if (ok.contains(key)) { 127 | entries.put(key, entry); 128 | } 129 | } 130 | } 131 | 132 | private EocdRecord readEocdRecord() throws IOException { 133 | boolean found = false; 134 | long length = _length(); 135 | long off = length - MIN_EOCD_SIZE; 136 | final long stopSearching = Math.max(0L, length - MAX_EOCD_SIZE); 137 | while (off >= stopSearching) { 138 | _seek(off); 139 | if (_readInt() == EOCD_SIG) { 140 | found = true; 141 | break; 142 | } 143 | off--; 144 | } 145 | if (!found) { 146 | return null; 147 | } 148 | 149 | try { 150 | final long zip64EocdRecordOffset = parseZip64EocdRecordLocator(off); 151 | 152 | EocdRecord record = parseEocdRecord(off + 4, (zip64EocdRecordOffset != -1) /* isZip64 */); 153 | if (record.commentLength > 0) { 154 | try { 155 | _readBytes(record.commentLength); 156 | } catch (IOException ignored) { 157 | record = new EocdRecord(record.numEntries, record.centralDirOffset, 0, record.zip64); 158 | } 159 | } 160 | 161 | if (zip64EocdRecordOffset != -1) { 162 | record = parseZip64EocdRecord(zip64EocdRecordOffset, record.commentLength); 163 | } 164 | 165 | return record; 166 | } catch (IOException e) { 167 | e.printStackTrace(); 168 | return null; 169 | } 170 | } 171 | 172 | private long parseZip64EocdRecordLocator(long eocdOffset) 173 | throws IOException { 174 | // The spec stays curiously silent about whether a zip file with an EOCD record, 175 | // a zip64 locator and a zip64 eocd record is considered "empty". In our implementation, 176 | // we parse all records and read the counts from them instead of drawing any size or 177 | // layout based information. 178 | if (eocdOffset > ZIP64_LOCATOR_SIZE) { 179 | _seek(eocdOffset - ZIP64_LOCATOR_SIZE); 180 | if (_readInt() == ZIP64_LOCATOR_SIGNATURE) { 181 | final int diskWithCentralDir = _readInt(); 182 | final long zip64EocdRecordOffset = _readLong(); 183 | final int numDisks = _readInt(); 184 | if (numDisks != 1 || diskWithCentralDir != 0) { 185 | throw new IOException("Spanned archives not supported"); 186 | } 187 | return zip64EocdRecordOffset; 188 | } 189 | } 190 | return -1; 191 | } 192 | 193 | private EocdRecord parseEocdRecord(long offset, boolean isZip64) throws IOException { 194 | _seek(offset); 195 | final long numEntries; 196 | final long centralDirOffset; 197 | if (isZip64) { 198 | numEntries = -1; 199 | centralDirOffset = -1; 200 | _skip(16); 201 | } else { 202 | _skip(4); 203 | numEntries = _readUShort(); 204 | _skip(6); 205 | centralDirOffset = _readUInt(); 206 | } 207 | final int commentLength = _readUShort(); 208 | return new EocdRecord(numEntries, centralDirOffset, commentLength, false); 209 | } 210 | 211 | private EocdRecord parseZip64EocdRecord(long eocdRecordOffset, int commentLength) throws IOException { 212 | _seek(eocdRecordOffset); 213 | final int signature = _readInt(); 214 | if (signature != ZIP64_EOCD_RECORD_SIGNATURE) { 215 | throw new IOException("Invalid zip64 eocd record offset, sig=" 216 | + Integer.toHexString(signature) + " offset=" + eocdRecordOffset); 217 | } 218 | _skip(12); 219 | int diskNumber = _readInt(); 220 | int diskWithCentralDirStart = _readInt(); 221 | long numEntries = _readLong(); 222 | long totalNumEntries = _readLong(); 223 | _readLong(); 224 | long centralDirOffset = _readLong(); 225 | if (numEntries != totalNumEntries || diskNumber != 0 || diskWithCentralDirStart != 0) { 226 | throw new IOException("Spanned archives not supported :" + 227 | " numEntries=" + numEntries + ", totalNumEntries=" + totalNumEntries + 228 | ", diskNumber=" + diskNumber + ", diskWithCentralDirStart=" + 229 | diskWithCentralDirStart); 230 | } 231 | return new EocdRecord(numEntries, centralDirOffset, commentLength, true); 232 | } 233 | 234 | public InputStream getRawInputStream(ZipEntry ze) { 235 | return new BridgeInputStream(archive, ze.getDataOffset(), ze.getCompressedSize()); 236 | } 237 | 238 | public InputStream getInputStream(ZipEntry ze) throws IOException { 239 | long start = ze.getDataOffset(); 240 | int method = ze.getMethod(); 241 | InputStream is = new BridgeInputStream(archive, start, method == METHOD_STORED ? ze.getSize() : ze.getCompressedSize()); 242 | switch (method) { 243 | case METHOD_DEFLATED: 244 | is = new NoWrapInflaterInputStream(ze, is); 245 | break; 246 | case METHOD_STORED: 247 | break; 248 | default: 249 | throw new IOException("Unsupported compression method " + ze.getMethod() + " (" + ze.getName() + ")"); 250 | } 251 | if (method != METHOD_STORED) { 252 | is = new BufferedInputStream(is, 64 * 1024); 253 | } 254 | return is; 255 | } 256 | 257 | public ZipFile openEntryAsZipFile(ZipEntry entry) throws IOException { 258 | if (entry.getMethod() != METHOD_STORED) { 259 | throw new IOException("Entry is not stored: " + entry.getName()); 260 | } 261 | return new ZipFile(archive.newFragment(entry.getDataOffset(), entry.getCompressedSize())); 262 | } 263 | 264 | public RandomAccessFile getArchive() { 265 | return archive; 266 | } 267 | 268 | private long _length() throws IOException { 269 | return archive.length(); 270 | } 271 | 272 | private void _seek(long position) throws IOException { 273 | archive.seek(position); 274 | } 275 | 276 | private void _skip(long length) throws IOException { 277 | if (length < 0) 278 | throw new IOException("Skip " + length); 279 | long pos = archive.getFilePointer() + length; 280 | long len = archive.length(); 281 | if (pos > len) 282 | throw new EOFException(); 283 | archive.seek(pos); 284 | } 285 | 286 | private byte[] _readBytes(int len) throws IOException { 287 | byte[] bytes = new byte[len]; 288 | archive.readFully(bytes); 289 | return bytes; 290 | } 291 | 292 | private int _readInt() throws IOException { 293 | int ch1 = archive.read(); 294 | int ch2 = archive.read(); 295 | int ch3 = archive.read(); 296 | int ch4 = archive.read(); 297 | if ((ch1 | ch2 | ch3 | ch4) < 0) 298 | throw new EOFException(); 299 | return (ch1) | (ch2 << 8) | (ch3 << 16) | (ch4 << 24); 300 | } 301 | 302 | private int _readUShort() throws IOException { 303 | int ch1 = archive.read(); 304 | int ch2 = archive.read(); 305 | if ((ch1 | ch2) < 0) 306 | throw new EOFException(); 307 | return ch1 | (ch2 << 8); 308 | } 309 | 310 | private long _readUInt() throws IOException { 311 | int value = _readInt(); 312 | return value & 0xFFFFFFFFL; 313 | } 314 | 315 | private long _readLong() throws IOException { 316 | return _readUInt() | (_readUInt() << 32); 317 | } 318 | 319 | private boolean closed = false; 320 | 321 | @Override 322 | public void close() throws IOException { 323 | if (closed) 324 | return; 325 | archive.close(); 326 | closed = true; 327 | } 328 | 329 | private static class EocdRecord { 330 | final long numEntries; 331 | final long centralDirOffset; 332 | final int commentLength; 333 | final boolean zip64; 334 | 335 | EocdRecord(long numEntries, long centralDirOffset, int commentLength, boolean zip64) { 336 | this.numEntries = numEntries; 337 | this.centralDirOffset = centralDirOffset; 338 | this.commentLength = commentLength; 339 | this.zip64 = zip64; 340 | } 341 | } 342 | 343 | } 344 | -------------------------------------------------------------------------------- /src/bin/zip/ZipMaker.java: -------------------------------------------------------------------------------- 1 | package bin.zip; 2 | 3 | import bin.io.RandomAccessFactory; 4 | import bin.io.RandomAccessFile; 5 | 6 | import java.io.*; 7 | import java.nio.charset.Charset; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Collections; 11 | import java.util.Objects; 12 | import java.util.zip.Deflater; 13 | 14 | import static bin.zip.ZipConstant.*; 15 | 16 | /** 17 | * @author Bin 18 | */ 19 | public class ZipMaker implements Closeable { 20 | public static final int LEVEL_FASTEST = Deflater.BEST_SPEED; 21 | public static final int LEVEL_FASTER = 3; 22 | public static final int LEVEL_DEFAULT = Deflater.DEFAULT_COMPRESSION; 23 | public static final int LEVEL_BETTER = 7; 24 | public static final int LEVEL_BEST = Deflater.BEST_COMPRESSION; 25 | 26 | public static final int METHOD_DEFLATED = ZipConstant.METHOD_DEFLATED; 27 | public static final int METHOD_STORED = ZipConstant.METHOD_STORED; 28 | 29 | private final RandomAccessFile archive; 30 | 31 | private final ArrayList headers = new ArrayList<>(); 32 | 33 | private CenterFileHeader currentHeader; 34 | 35 | private int method = METHOD_DEFLATED; 36 | 37 | private int level = LEVEL_DEFAULT; 38 | 39 | private Charset encoding = ZipConstant.UTF_8; 40 | 41 | private String comment; 42 | 43 | private boolean needsZip64EocdRecord; 44 | 45 | private boolean forceZip64; 46 | 47 | private CrcOutputStream topOutput; 48 | 49 | private BridgeOutputStream bottomOutput; 50 | 51 | public ZipMaker(String path) throws IOException { 52 | this(new File(path)); 53 | } 54 | 55 | public ZipMaker(File file) throws IOException { 56 | if (file.exists()) 57 | //noinspection ResultOfMethodCallIgnored 58 | file.delete(); 59 | this.archive = RandomAccessFactory.from(file, "rw"); 60 | } 61 | 62 | public void setForceZip64(boolean forceZip64) { 63 | this.forceZip64 = forceZip64; 64 | } 65 | 66 | public void setEncoding(Charset encoding) { 67 | this.encoding = encoding; 68 | } 69 | 70 | public void setEncoding(String encoding) { 71 | this.encoding = Charset.forName(encoding); 72 | } 73 | 74 | public void setMethod(int method) { 75 | this.method = method; 76 | } 77 | 78 | public int getMethod() { 79 | return method; 80 | } 81 | 82 | public void setLevel(int level) { 83 | this.level = level; 84 | } 85 | 86 | public int getLevel() { 87 | return level; 88 | } 89 | 90 | public void setComment(String comment) { 91 | this.comment = comment; 92 | } 93 | 94 | public String getComment() { 95 | return comment; 96 | } 97 | 98 | private final byte[] copyEntryBuffer = new byte[8 * 1024]; 99 | 100 | public void putNextEntry(String name) throws IOException { 101 | putNextEntry(new CenterFileHeader(name)); 102 | } 103 | 104 | public void putNextEntry(ZipEntry ze) throws IOException { 105 | putNextEntry(new CenterFileHeader(ze)); 106 | } 107 | 108 | private void putNextEntry(CenterFileHeader header) throws IOException { 109 | if (currentHeader != null) { 110 | closeEntry(); 111 | } 112 | header.headerOffset = _getFilePointer(); 113 | headers.add(header); 114 | 115 | if (!header.isDirectory) { 116 | currentHeader = header; 117 | 118 | int generalPurposeFlag = 0; 119 | int method = this.method; 120 | 121 | bottomOutput = new BridgeOutputStream(archive); 122 | OutputStream os = bottomOutput; 123 | 124 | if (header.isUtf8) 125 | generalPurposeFlag |= UFT8_NAMES_FLAG; 126 | 127 | switch (this.method) { 128 | case METHOD_DEFLATED: 129 | os = new NoWrapDeflaterOutputStream(os, level); 130 | break; 131 | case METHOD_STORED: 132 | break; 133 | default: 134 | throw new IOException("Unsupported compression method " + method); 135 | } 136 | 137 | topOutput = new CrcOutputStream(os); 138 | 139 | header.generalPurposeFlag = generalPurposeFlag; 140 | header.method = method; 141 | } else { 142 | header.method = METHOD_STORED; 143 | if (header.isUtf8) { 144 | header.generalPurposeFlag = UFT8_NAMES_FLAG; 145 | } 146 | } 147 | 148 | writeHeader(header); 149 | header.dataOffset = _getFilePointer(); 150 | } 151 | 152 | public void putNextRawEntry(ZipEntry ze) throws IOException { 153 | if (currentHeader != null) 154 | closeEntry(); 155 | CenterFileHeader header = new CenterFileHeader(ze); 156 | if (header.isUtf8) 157 | header.generalPurposeFlag |= UFT8_NAMES_FLAG; 158 | header.headerOffset = _getFilePointer(); 159 | headers.add(header); 160 | writeHeader(header); 161 | header.dataOffset = _getFilePointer(); 162 | } 163 | 164 | public void writeRaw(byte[] data) throws IOException { 165 | writeRaw(data, 0, data.length); 166 | } 167 | 168 | public void writeRaw(byte[] data, int off, int len) throws IOException { 169 | archive.write(data, off, len); 170 | } 171 | 172 | public void copyZipEntry(ZipEntry ze, ZipFile zipFile) throws IOException { 173 | putNextRawEntry(ze); 174 | if (!ze.isDirectory()) { 175 | InputStream is = zipFile.getRawInputStream(ze); 176 | byte[] buffer = copyEntryBuffer; 177 | int len; 178 | while ((len = is.read(buffer)) != -1) { 179 | writeRaw(buffer, 0, len); 180 | } 181 | } 182 | } 183 | 184 | public HostEntryHolder putNextHostEntry(String name, ZipFile zipFile) throws IOException { 185 | if (name.endsWith("/") || name.endsWith("\\")) { 186 | throw new IOException("Invalid host entry name: " + name); 187 | } 188 | int savedMethod = method; 189 | method = METHOD_STORED; 190 | CenterFileHeader centerFileHeader = new CenterFileHeader(name); 191 | centerFileHeader.isHost = true; 192 | putNextEntry(centerFileHeader); 193 | method = savedMethod; 194 | return new HostEntryHolder(zipFile); 195 | } 196 | 197 | public class HostEntryHolder { 198 | private final CenterFileHeader hostHeader; 199 | private final ZipFile zipFile; 200 | 201 | private HostEntryHolder(ZipFile zipFile) throws IOException { 202 | this.hostHeader = Objects.requireNonNull(currentHeader); 203 | this.zipFile = zipFile; 204 | try (RandomAccessFile archive = zipFile.getArchive().newSameInstance()) { 205 | writeFully(new BridgeInputStream(archive, 0, archive.length())); 206 | closeEntry(); 207 | } 208 | } 209 | 210 | public long getHostEntryHeaderOffset() { 211 | return hostHeader.headerOffset; 212 | } 213 | 214 | /** 215 | * @return virtualEntry.headerOffset - hostHeader.headerOffset 216 | */ 217 | public long putNextVirtualEntry(String name) throws IOException { 218 | ZipEntry innerEntry = zipFile.getEntryNonNull(name); 219 | CenterFileHeader header = new CenterFileHeader(innerEntry); 220 | setupNeedZip64(header); 221 | header.headerOffset = innerEntry.getHeaderOffset() + hostHeader.dataOffset; 222 | header.dataOffset = innerEntry.getDataOffset() + hostHeader.dataOffset; 223 | headers.add(header); 224 | return header.headerOffset - hostHeader.headerOffset; 225 | } 226 | } 227 | 228 | private void writeHeader(CenterFileHeader header) throws IOException { 229 | setupNeedZip64(header); 230 | 231 | _writeInt(LFH_SIG); 232 | _writeShort(header.version()); 233 | _writeShort(header.generalPurposeFlag); 234 | _writeShort(header.method); 235 | _writeInt(header.time); 236 | _writeInt(header.crc); 237 | if (header.sizeNeedZip64) { 238 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE); 239 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE); 240 | } else { 241 | _writeUInt(header.compressedSize); 242 | _writeUInt(header.size); 243 | } 244 | _writeShort(header.name.length); 245 | 246 | byte[] extra; 247 | if (header.sizeNeedZip64) { 248 | byte[] data = new byte[2 * 8]; 249 | ZipUtil.writeLong(data, 0, header.size); 250 | ZipUtil.writeLong(data, 8, header.compressedSize); 251 | extra = ExtraDataRecord.set(header.extra, ZIP64_EXTENDED_INFO_HEADER_ID, data); 252 | } else { 253 | extra = ExtraDataRecord.remove(header.extra, ZIP64_EXTENDED_INFO_HEADER_ID); 254 | } 255 | // zipAlign 256 | if (header.method == METHOD_STORED) { 257 | int alignment; 258 | if (header.isHost || new String(header.name, ZipConstant.UTF_8).endsWith(".so")) { 259 | // -p: memory page alignment for stored shared object files 260 | alignment = 4096; 261 | } else { 262 | alignment = 4; 263 | } 264 | long extraDataOffset = _getFilePointer() + 2 + header.name.length; 265 | extra = align(alignment, extra, extraDataOffset); 266 | } 267 | _writeShort(extra.length); 268 | _writeBytes(header.name); 269 | 270 | _writeBytes(extra); 271 | } 272 | 273 | private void setupNeedZip64(CenterFileHeader header) { 274 | if (forceZip64) { 275 | header.sizeNeedZip64 = true; 276 | header.offsetNeedZip64 = true; 277 | } else { 278 | // 只知道原体积但不知道压缩后体积时,若原体积大于0xf0000000L则采用zip64 279 | if (header.size >= 0xf0000000L && header.compressedSize == ZipEntry.UNKNOWN_SIZE) { 280 | header.sizeNeedZip64 = true; 281 | } else if (header.size >= MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE || 282 | header.compressedSize >= MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) { 283 | header.sizeNeedZip64 = true; 284 | } 285 | if (header.headerOffset >= MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) 286 | header.offsetNeedZip64 = true; 287 | } 288 | if (header.needZip64()) { 289 | needsZip64EocdRecord = true; 290 | } 291 | if (header.size == ZipEntry.UNKNOWN_SIZE) { 292 | header.size = 0; 293 | } 294 | if (header.compressedSize == ZipEntry.UNKNOWN_SIZE) { 295 | header.compressedSize = 0; 296 | } 297 | } 298 | 299 | public void write(int b) throws IOException { 300 | topOutput.write(b); 301 | } 302 | 303 | public void write(byte[] data) throws IOException { 304 | topOutput.write(data); 305 | } 306 | 307 | public void write(byte[] data, int off, int len) throws IOException { 308 | topOutput.write(data, off, len); 309 | } 310 | 311 | public void writeFully(InputStream is) throws IOException { 312 | int len; 313 | byte[] b = new byte[1024 * 4]; 314 | while ((len = is.read(b)) > 0) 315 | write(b, 0, len); 316 | } 317 | 318 | public void closeEntry() throws IOException { 319 | if (currentHeader == null) { 320 | return; 321 | } 322 | topOutput.close(); 323 | 324 | currentHeader.crc = topOutput.getCrc(); 325 | currentHeader.compressedSize = bottomOutput.getCount(); 326 | currentHeader.size = topOutput.getCount(); 327 | 328 | long saved = _getFilePointer(); 329 | _seek(currentHeader.headerOffset + WORD + SHORT + SHORT + SHORT + WORD); 330 | _writeInt(currentHeader.crc); 331 | if (currentHeader.sizeNeedZip64) { 332 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE); 333 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE); 334 | 335 | // skip nameLength + extraLength + nameData 336 | _skip(SHORT + SHORT + currentHeader.name.length); 337 | 338 | // skip zip64Extra(header + size) 339 | _skip(4); 340 | 341 | // update local extra 342 | _writeLong(currentHeader.size); 343 | _writeLong(currentHeader.compressedSize); 344 | } else { 345 | if (currentHeader.compressedSize >= MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE || 346 | currentHeader.size >= MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) { 347 | throw new IOException("Zip entry size needs zip64: name=" + new String(currentHeader.name) 348 | + ", compressedSize=" + currentHeader.compressedSize 349 | + ", size=" + currentHeader.size 350 | ); 351 | } 352 | _writeUInt(currentHeader.compressedSize); 353 | _writeUInt(currentHeader.size); 354 | } 355 | 356 | archive.seek(saved); 357 | 358 | topOutput = null; 359 | bottomOutput = null; 360 | currentHeader = null; 361 | } 362 | 363 | @Override 364 | public void close() throws IOException { 365 | if (archive.isClosed()) 366 | return; 367 | if (currentHeader != null) 368 | closeEntry(); 369 | long cdOffset = _getFilePointer(); 370 | try { 371 | Collections.sort(headers); 372 | } catch (RuntimeException e) { 373 | throw new IOException(e); 374 | } 375 | for (CenterFileHeader header : headers) { 376 | writeCentralFileHeader(header); 377 | } 378 | long cdSize = _getFilePointer() - cdOffset; 379 | writeCentralDirectoryEnd(cdSize, cdOffset); 380 | archive.close(); 381 | } 382 | 383 | private byte[] align(int alignment, byte[] extra, long extraDataOffset) throws IOException { 384 | if (isAligned(extraDataOffset + extra.length, alignment)) { 385 | return extra; 386 | } 387 | extra = ExtraDataRecord.trim(extra); 388 | int padding = getAlignedPadding(extraDataOffset + extra.length, alignment); 389 | return Arrays.copyOf(extra, extra.length + padding); 390 | } 391 | 392 | private static boolean isAligned(long pos, int alignTo) { 393 | return (pos % alignTo) == 0; 394 | } 395 | 396 | private static int getAlignedPadding(long pos, int alignTo) { 397 | return (int) (alignTo - (pos % alignTo)) % alignTo; 398 | } 399 | 400 | private void writeCentralFileHeader(CenterFileHeader header) throws IOException { 401 | boolean needZip64 = header.needZip64(); 402 | byte[] extra; 403 | if (needZip64) { 404 | byte[] data = new byte[3 * 8]; 405 | ZipUtil.writeLong(data, 0, header.size); 406 | ZipUtil.writeLong(data, 8, header.compressedSize); 407 | ZipUtil.writeLong(data, 16, header.headerOffset); 408 | extra = ExtraDataRecord.set(header.extra, ZIP64_EXTENDED_INFO_HEADER_ID, data); 409 | } else { 410 | extra = ExtraDataRecord.remove(header.extra, ZIP64_EXTENDED_INFO_HEADER_ID); 411 | } 412 | 413 | _writeInt(CFH_SIG); 414 | _writeShort(Math.max(20, header.version())); 415 | _writeShort(header.version()); 416 | _writeShort(header.generalPurposeFlag); 417 | _writeShort(header.method); 418 | _writeInt(header.time); 419 | _writeInt(header.crc); 420 | if (needZip64) { 421 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE); 422 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE); 423 | } else { 424 | _writeUInt(header.compressedSize); 425 | _writeUInt(header.size); 426 | } 427 | _writeShort(header.name.length); 428 | _writeShort(extra.length); 429 | _writeShort(header.comment.length); 430 | _writeShort(header.diskNumberStart); 431 | _writeShort(header.internalAttributes); 432 | _writeInt(header.externalAttributes); 433 | if (needZip64) { 434 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE); 435 | } else { 436 | _writeUInt(header.headerOffset); 437 | } 438 | _writeBytes(header.name); 439 | _writeBytes(extra); 440 | _writeBytes(header.comment); 441 | } 442 | 443 | private void writeCentralDirectoryEnd(long cdSize, long cdOffset) throws IOException { 444 | if (headers.size() >= 0xffff) { 445 | needsZip64EocdRecord = true; 446 | } 447 | if (needsZip64EocdRecord) { 448 | // Zip64 end of central directory record 449 | _writeInt(ZIP64_EOCD_RECORD_SIGNATURE); 450 | _writeLong(ZIP64_EOCD_RECORD_EFFECTIVE_SIZE + 4); 451 | _writeShort(20); 452 | _writeShort(20); 453 | _writeInt(0); // number of disk 454 | _writeInt(0); // number of disk with start of central dir. 455 | _writeLong(headers.size()); 456 | _writeLong(headers.size()); 457 | _writeLong(cdSize); 458 | _writeLong(cdOffset); 459 | 460 | // Zip64 end of central directory locator 461 | _writeInt(ZIP64_LOCATOR_SIGNATURE); 462 | _writeInt(0); 463 | _writeLong(cdSize + cdOffset); 464 | _writeInt(1); 465 | } 466 | byte[] comment = this.comment == null ? new byte[0] : this.comment.getBytes(encoding); 467 | _writeInt(EOCD_SIG); 468 | _writeShort(0); 469 | _writeShort(0); 470 | if (needsZip64EocdRecord) { 471 | _writeShort(0xFFFF); 472 | _writeShort(0xFFFF); 473 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE); 474 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE); 475 | } else { 476 | _writeShort(headers.size()); 477 | _writeShort(headers.size()); 478 | _writeUInt(cdSize); 479 | _writeUInt(cdOffset); 480 | } 481 | _writeShort(comment.length); 482 | _writeBytes(comment); 483 | } 484 | 485 | private void _seek(long position) throws IOException { 486 | archive.seek(position); 487 | } 488 | 489 | private long _getFilePointer() throws IOException { 490 | return archive.getFilePointer(); 491 | } 492 | 493 | private void _skip(int n) throws IOException { 494 | archive.skipBytes(n); 495 | } 496 | 497 | private void _writeBytes(byte[] data) throws IOException { 498 | if (data.length > 0) 499 | archive.write(data); 500 | } 501 | 502 | private void _writeShort(int v) throws IOException { 503 | archive.write(v & 0xFF); 504 | archive.write((v >>> 8) & 0xFF); 505 | } 506 | 507 | private void _writeInt(int v) throws IOException { 508 | archive.write(v & 0xFF); 509 | archive.write((v >>> 8) & 0xFF); 510 | archive.write((v >>> 16) & 0xFF); 511 | archive.write((v >>> 24) & 0xFF); 512 | } 513 | 514 | private void _writeLong(long v) throws IOException { 515 | archive.write((int) (v & 0xFF)); 516 | archive.write((int) ((v >>> 8) & 0xFF)); 517 | archive.write((int) ((v >>> 16) & 0xFF)); 518 | archive.write((int) ((v >>> 24) & 0xFF)); 519 | archive.write((int) ((v >>> 32) & 0xFF)); 520 | archive.write((int) ((v >>> 40) & 0xFF)); 521 | archive.write((int) ((v >>> 48) & 0xFF)); 522 | archive.write((int) ((v >>> 56) & 0xFF)); 523 | } 524 | 525 | private void _writeUInt(long v) throws IOException { 526 | if (v < 0 || v > 0xffffffffL) { 527 | throw new IOException("Value out of unsigned int."); 528 | } 529 | archive.write((int) (v & 0xFF)); 530 | archive.write((int) ((v >>> 8) & 0xFF)); 531 | archive.write((int) ((v >>> 16) & 0xFF)); 532 | archive.write((int) ((v >>> 24) & 0xFF)); 533 | } 534 | } 535 | -------------------------------------------------------------------------------- /src/bin/zip/ZipUtil.java: -------------------------------------------------------------------------------- 1 | package bin.zip; 2 | 3 | import java.io.EOFException; 4 | import java.io.IOException; 5 | import java.util.Calendar; 6 | 7 | /** 8 | * @author Bin 9 | */ 10 | public class ZipUtil { 11 | private static final Calendar CALENDAR = Calendar.getInstance(); 12 | 13 | public static long dosToJavaTime(long dosTime) { 14 | synchronized (CALENDAR) { 15 | CALENDAR.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980); 16 | CALENDAR.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1); 17 | CALENDAR.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f); 18 | CALENDAR.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f); 19 | CALENDAR.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f); 20 | CALENDAR.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e); 21 | return CALENDAR.getTime().getTime(); 22 | } 23 | } 24 | 25 | public static long javaToDosTime(long time) { 26 | Calendar cal = Calendar.getInstance(); 27 | cal.setTimeInMillis(time); 28 | int year = cal.get(Calendar.YEAR); 29 | if (year < 1980) { 30 | return (1 << 21) | (1 << 16); 31 | } 32 | return (year - 1980L) << 25 | (cal.get(Calendar.MONTH) + 1) << 21 | 33 | cal.get(Calendar.DATE) << 16 | cal.get(Calendar.HOUR_OF_DAY) << 11 | cal.get(Calendar.MINUTE) << 5 | 34 | cal.get(Calendar.SECOND) >> 1; 35 | } 36 | 37 | public static void writeByte(byte[] array, int pos, int value) throws IOException { 38 | if (pos + 1 > array.length) { 39 | throw new EOFException(); 40 | } 41 | array[pos] = (byte) (value & 0xFF); 42 | } 43 | 44 | public static void writeShort(byte[] array, int pos, int value) throws IOException { 45 | if (pos + 2 > array.length) { 46 | throw new EOFException(); 47 | } 48 | array[pos] = (byte) (value & 0xFF); 49 | array[pos + 1] = (byte) (value >>> 8 & 0xFF); 50 | } 51 | 52 | public static void writeLong(byte[] array, int pos, long value) throws IOException { 53 | if (pos + 8 > array.length) { 54 | throw new EOFException(); 55 | } 56 | array[pos] = (byte) (value & 0xFF); 57 | array[pos + 1] = (byte) (value >>> 8 & 0xFF); 58 | array[pos + 2] = (byte) (value >>> 16 & 0xFF); 59 | array[pos + 3] = (byte) (value >>> 24 & 0xFF); 60 | array[pos + 4] = (byte) (value >>> 32 & 0xFF); 61 | array[pos + 5] = (byte) (value >>> 40 & 0xFF); 62 | array[pos + 6] = (byte) (value >>> 48 & 0xFF); 63 | array[pos + 7] = (byte) (value >>> 56 & 0xFF); 64 | } 65 | 66 | public static void writeBytes(byte[] array, int pos, byte[] value) throws IOException { 67 | if (pos + value.length > array.length) { 68 | throw new EOFException(); 69 | } 70 | System.arraycopy(value, 0, array, pos, value.length); 71 | } 72 | 73 | public static int readUByte(byte[] b, int off) throws IOException { 74 | if (off + 1 > b.length) { 75 | throw new EOFException(); 76 | } 77 | return (b[off] & 0xff); 78 | } 79 | 80 | public static int readUShort(byte[] b, int off) throws IOException { 81 | if (off + 2 > b.length) { 82 | throw new EOFException(); 83 | } 84 | return (b[off + 1] & 0xFF) << 8 | b[off] & 0xFF; 85 | } 86 | 87 | public static int readInt(byte[] b, int off) throws IOException { 88 | if (off + 4 > b.length) { 89 | throw new EOFException(); 90 | } 91 | return b[off + 3] << 24 | (b[off + 2] & 0xFF) << 16 | (b[off + 1] & 0xFF) << 8 | b[off] & 0xFF; 92 | } 93 | 94 | public static long readLong(byte[] b, int off) throws IOException { 95 | if (off + 8 > b.length) { 96 | throw new EOFException(); 97 | } 98 | return (long) b[off + 7] << 56 | ((long) b[off + 6] & 0xFF) << 48 99 | | ((long) b[off + 5] & 0xFF) << 40 | ((long) b[off + 4] & 0xFF) << 32 100 | | ((long) b[off + 3] & 0xFF) << 24 | ((long) b[off + 2] & 0xFF) << 16 101 | | ((long) b[off + 1] & 0xFF) << 8 | (long) b[off] & 0xFF; 102 | } 103 | 104 | 105 | } 106 | -------------------------------------------------------------------------------- /test.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/L-JINBIN/ApkDataMultiplexing/0887b62c24df53f68ded273e04934163ef3dcd94/test.apk -------------------------------------------------------------------------------- /test.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/L-JINBIN/ApkDataMultiplexing/0887b62c24df53f68ded273e04934163ef3dcd94/test.jks --------------------------------------------------------------------------------