implement APK signature block reading

This commit is contained in:
REAndroid 2023-04-03 17:22:42 -04:00
parent 3b669f6f92
commit 2eb4e7f96d
12 changed files with 459 additions and 58 deletions

View File

@ -166,12 +166,12 @@ public class ApkModule implements ApkFile {
return null;
}
AndroidManifestBlock manifestBlock = getAndroidManifestBlock();
Integer version = manifestBlock.getCompileSdkVersion();
Integer version = manifestBlock.getTargetSdkVersion();
if(version == null){
version = manifestBlock.getPlatformBuildVersionCode();
}
if(version == null){
version = manifestBlock.getTargetSdkVersion();
version = manifestBlock.getCompileSdkVersion();
}
return version;
}

View File

@ -21,6 +21,7 @@ import com.reandroid.archive2.block.LocalFileHeader;
import com.reandroid.archive2.io.ArchiveFile;
import com.reandroid.archive2.io.ArchiveUtil;
import com.reandroid.archive2.io.ZipSource;
import com.reandroid.archive2.model.ApkSigBlock;
import com.reandroid.archive2.model.LocalFileDirectory;
import java.io.File;
@ -37,6 +38,7 @@ public class Archive {
private final ZipSource zipSource;
private final List<ArchiveEntry> entryList;
private final EndRecord endRecord;
private final ApkSigBlock apkSigBlock;
public Archive(ZipSource zipSource) throws IOException {
this.zipSource = zipSource;
LocalFileDirectory lfd = new LocalFileDirectory();
@ -52,6 +54,7 @@ public class Archive {
}
this.entryList = entryList;
this.endRecord = lfd.getCentralFileDirectory().getEndRecord();
this.apkSigBlock = lfd.getApkSigBlock();
}
public Archive(File file) throws IOException {
this(new ArchiveFile(file));
@ -71,6 +74,13 @@ public class Archive {
return entryList;
}
public ApkSigBlock getApkSigBlock() {
return apkSigBlock;
}
public EndRecord getEndRecord() {
return endRecord;
}
// for test
public void extract(File dir) throws IOException {
for(ArchiveEntry archiveEntry:getEntryList()){

View File

@ -0,0 +1,202 @@
/*
* Copyright (C) 2022 github.com/REAndroid
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.reandroid.archive2.block;
import com.reandroid.arsc.decoder.ValueDecoder;
import java.io.*;
import java.util.Objects;
public class ApkSignature extends ZipBlock{
public ApkSignature() {
super(MIN_SIZE);
}
public boolean isValid(){
return getSigSize() > MIN_SIZE && getId() != null;
}
@Override
public int readBytes(InputStream inputStream) throws IOException {
setBytesLength(8, false);
byte[] bytes = getBytesInternal();
int read = inputStream.read(bytes, 0, bytes.length);
if(read != bytes.length){
return read;
}
int offset = bytes.length;
int length = (int) getSigSize();
setSigSize(length);
bytes = getBytesInternal();
int dataRead = inputStream.read(bytes, offset, length);
if(dataRead != length){
setSigSize(0);
}
return read + dataRead;
}
public long getSigSize(){
return getLong(OFFSET_size);
}
public void setSigSize(long size){
int length = (int) (OFFSET_id + size);
setBytesLength(length, false);
putLong(OFFSET_size, size);
}
public int getIdValue(){
return getInteger(OFFSET_id);
}
public Id getId(){
return Id.valueOf(getIdValue());
}
public void setId(int id){
putInteger(OFFSET_id, id);
}
public void setId(Id signatureId){
setId(signatureId == null? 0 : signatureId.getId());
}
public byte[] getData(){
return getBytes(OFFSET_data, (int) getSigSize() - 4, true);
}
public void setData(byte[] data){
if(data == null){
data = new byte[0];
}
setData(data, 0, data.length);
}
public void setData(byte[] data, int offset, int length){
setSigSize(length);
putBytes(data, offset, OFFSET_data, data.length);
}
public void writeData(OutputStream outputStream) throws IOException{
byte[] bytes = getBytesInternal();
outputStream.write(bytes, OFFSET_data, (int) (getSigSize() - 4));
}
public void writeData(File file) throws IOException{
File dir = file.getParentFile();
if(dir != null && !dir.exists()){
dir.mkdirs();
}
FileOutputStream outputStream = new FileOutputStream(file);
writeData(outputStream);
outputStream.close();
}
public File writeToDir(File dir) throws IOException{
File file = new File(dir, getId().toFileName());
writeData(file);
return file;
}
@Override
public String toString() {
return getId() + ", size=" + getSigSize();
}
public static class Id {
private final String name;
private final int id;
private Id(String name, int id) {
this.name = name;
this.id = id;
}
public String name() {
return name;
}
public int getId() {
return id;
}
public String toFileName(){
if(this.name != null){
return name + FILE_EXTENSION;
}
return String.format("0x%08x", id) + FILE_EXTENSION;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Id that = (Id) obj;
return id == that.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString(){
String name = this.name;
if(name != null){
return name;
}
return "UNKNOWN("+String.format("0x%08x", id)+")";
}
public static Id valueOf(String name){
if(name == null){
return null;
}
String ext = FILE_EXTENSION;
if(name.endsWith(ext)){
name = name.substring(0, name.length()-ext.length());
}
for(Id signatureId:VALUES){
if(name.equalsIgnoreCase(signatureId.name())){
return signatureId;
}
}
if(ValueDecoder.isHex(name)){
return new Id(null, ValueDecoder.parseHex(name));
}
return null;
}
public static Id valueOf(int id){
if(id == 0){
return null;
}
for(Id signatureId:VALUES){
if(id == signatureId.getId()){
return signatureId;
}
}
return new Id(null, id);
}
public static Id[] values() {
return VALUES.clone();
}
public static final Id V2 = new Id("V2", 0x7109871A);
public static final Id V3 = new Id("V3",0xF05368C0);
public static final Id V31 = new Id("V31",0x1B93AD61);
public static final Id STAMP_V1 = new Id("STAMP_V1", 0x2B09189E);
public static final Id STAMP_V2 = new Id("STAMP_V2", 0x6DFF800D);
public static final Id PADDING = new Id("PADDING", 0x42726577);
private static final Id[] VALUES = new Id[]{
V2, V3, V31, STAMP_V1, STAMP_V2, PADDING
};
private static final String FILE_EXTENSION = ".bin";
}
public static final int MIN_SIZE = 12;
private static final int OFFSET_size = 0;
private static final int OFFSET_id = 8;
private static final int OFFSET_data = 12;
}

View File

@ -115,7 +115,7 @@ public abstract class CommonHeader extends ZipHeader {
putShort(offsetGeneralPurpose + 2, value);
}
public long getDosTime(){
return getUnsignedLong(offsetGeneralPurpose + 4);
return getIntegerUnsigned(offsetGeneralPurpose + 4);
}
public void setDosTime(long value){
putInteger(offsetGeneralPurpose + 4, value);
@ -130,19 +130,19 @@ public abstract class CommonHeader extends ZipHeader {
setDosTime(javaToDosTime(date));
}
public long getCrc(){
return getUnsignedLong(offsetGeneralPurpose + 8);
return getIntegerUnsigned(offsetGeneralPurpose + 8);
}
public void setCrc(long value){
putInteger(offsetGeneralPurpose + 8, value);
}
public long getCompressedSize(){
return getUnsignedLong(offsetGeneralPurpose + 12);
return getIntegerUnsigned(offsetGeneralPurpose + 12);
}
public void setCompressedSize(long value){
putInteger(offsetGeneralPurpose + 12, value);
}
public long getSize(){
return getUnsignedLong(offsetGeneralPurpose + 16);
return getIntegerUnsigned(offsetGeneralPurpose + 16);
}
public void setSize(long value){
putInteger(offsetGeneralPurpose + 16, value);

View File

@ -22,19 +22,19 @@ public class DataDescriptor extends ZipHeader{
super(MIN_LENGTH, ZipSignature.DATA_DESCRIPTOR);
}
public long getCrc(){
return getUnsignedLong(OFFSET_crc);
return getIntegerUnsigned(OFFSET_crc);
}
public void setCrc(long value){
putInteger(OFFSET_crc, value);
}
public long getCompressedSize(){
return getUnsignedLong(OFFSET_compressed_size);
return getIntegerUnsigned(OFFSET_compressed_size);
}
public void setCompressedSize(long value){
putInteger(OFFSET_compressed_size, value);
}
public long getSize(){
return getUnsignedLong(OFFSET_size);
return getIntegerUnsigned(OFFSET_size);
}
public void setSize(long value){
putInteger(OFFSET_size, value);

View File

@ -46,13 +46,13 @@ public class EndRecord extends ZipHeader{
putShort(OFFSET_totalNumberOfDirectories, value);
}
public long getLengthOfCentralDirectory(){
return getUnsignedLong(OFFSET_lengthOfCentralDirectory);
return getIntegerUnsigned(OFFSET_lengthOfCentralDirectory);
}
public void setLengthOfCentralDirectory(long value){
putInteger(OFFSET_lengthOfCentralDirectory, value);
}
public long getOffsetOfCentralDirectory(){
return getUnsignedLong(OFFSET_offsetOfCentralDirectory);
return getIntegerUnsigned(OFFSET_offsetOfCentralDirectory);
}
public void setOffsetOfCentralDirectory(int value){
putInteger(OFFSET_offsetOfCentralDirectory, value);

View File

@ -0,0 +1,40 @@
/*
* Copyright (C) 2022 github.com/REAndroid
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.reandroid.archive2.block;
import java.io.IOException;
import java.io.InputStream;
public class LongBlock extends ZipBlock{
public LongBlock() {
super(8);
}
@Override
public int readBytes(InputStream inputStream) throws IOException {
byte[] bytes = getBytesInternal();
return inputStream.read(bytes, 0, bytes.length);
}
public long get(){
return getLong(0);
}
public void set(long value){
putLong(0, value);
}
@Override
public String toString(){
return String.valueOf(get());
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright (C) 2022 github.com/REAndroid
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.reandroid.archive2.block;
import com.reandroid.arsc.item.ByteArray;
import java.io.IOException;
import java.io.InputStream;
public class SignatureFooter extends ZipBlock{
public SignatureFooter() {
super(MIN_SIZE);
setMagic(APK_SIG_BLOCK_MAGIC);
}
@Override
public int readBytes(InputStream inputStream) throws IOException {
setBytesLength(MIN_SIZE, false);
byte[] bytes = getBytesInternal();
return inputStream.read(bytes, 0, bytes.length);
}
public long getSigBlockSizeInFooter(){
return getLong(OFFSET_size);
}
public void setSigBlockSizeInFooter(long size){
putLong(OFFSET_size, size);
}
public byte[] getMagic() {
return getBytes(OFFSET_magic, APK_SIG_BLOCK_MAGIC.length, false);
}
public void setMagic(byte[] magic){
putBytes(magic, 0, OFFSET_magic, magic.length);
}
public boolean isValid(){
return getSigBlockSizeInFooter() > MIN_SIZE
&& ByteArray.equals(APK_SIG_BLOCK_MAGIC, getMagic());
}
@Override
public String toString(){
return getSigBlockSizeInFooter() + " ["+new String(getMagic())+"]";
}
public static final int MIN_SIZE = 24;
private static final int OFFSET_size = 0;
private static final int OFFSET_magic = 8;
private static final byte[] APK_SIG_BLOCK_MAGIC =
new byte[]{'A', 'P', 'K', ' ', 'S', 'i', 'g', ' ', 'B', 'l', 'o', 'c', 'k', ' ', '4', '2'};
}

View File

@ -43,7 +43,34 @@ public abstract class ZipBlock extends BlockItem {
this.readBytes((InputStream) blockReader);
}
long getUnsignedLong(int offset){
byte[] getBytes(int offset, int length, boolean strict){
byte[] bytes = getBytesInternal();
if(strict){
if(offset<0 || offset>=bytes.length || (offset + length)>bytes.length){
return null;
}
}
if(offset < 0){
offset = 0;
}
int available = bytes.length - offset;
if(length<=0 || available <=0){
return new byte[0];
}
if(length > available){
length = available;
}
byte[] result = new byte[length];
System.arraycopy(getBytesInternal(), offset, result, 0, length);
return result;
}
long getLong(int offset){
return getLong(getBytesInternal(), offset);
}
void putLong(int offset, long value){
putLong(getBytesInternal(), offset, value);
}
long getIntegerUnsigned(int offset){
return getInteger(offset) & 0x00000000ffffffffL;
}
void putBit(int offset, int bitIndex, boolean bit){
@ -71,4 +98,29 @@ public abstract class ZipBlock extends BlockItem {
putShort(getBytesInternal(), offset, (short) value);
}
public static long getLong(byte[] bytes, int offset){
if((offset + 8)>bytes.length){
return 0;
}
long result = 0;
int index = offset + 7;
while (index>=offset){
result = result << 8;
result |= (bytes[index] & 0xff);
index --;
}
return result;
}
public static void putLong(byte[] bytes, int offset, long value){
if((offset + 8) > bytes.length){
return;
}
int index = offset;
offset = index + 8;
while (index<offset){
bytes[index] = (byte) (value & 0xff);
value = value >>> 8;
index++;
}
}
}

View File

@ -15,45 +15,56 @@
*/
package com.reandroid.archive2.model;
import com.reandroid.archive2.block.ApkSignature;
import com.reandroid.archive2.block.LongBlock;
import com.reandroid.archive2.block.SignatureFooter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class ApkSigBlock {
public ApkSigBlock(){
private final SignatureFooter signatureFooter;
private final LongBlock mSize;
private final List<ApkSignature> mSignatures;
public ApkSigBlock(SignatureFooter signatureFooter){
this.signatureFooter = signatureFooter;
this.mSize = new LongBlock();
this.mSignatures = new ArrayList<>();
}
//TODO : implement all
public void parse(byte[] bytes) throws IOException {
int offset = findSignature(bytes);
if(offset<0){
return;
public LongBlock getTotalSize(){
return mSize;
}
offset += APK_SIGNING_BLOCK_MAGIC.length;
int length = bytes.length - offset;
public List<ApkSignature> getSignatures() {
return mSignatures;
}
private int findSignature(byte[] bytes){
for(int i=0;i<bytes.length;i++){
if (matchesSignature(bytes, i)){
return i;
public void readBytes(InputStream inputStream) throws IOException {
mSize.readBytes(inputStream);
ApkSignature apkSignature;
while ((apkSignature = readNext(inputStream))!=null){
mSignatures.add(apkSignature);
}
}
return -1;
private ApkSignature readNext(InputStream inputStream) throws IOException {
ApkSignature apkSignature = new ApkSignature();
apkSignature.readBytes(inputStream);
if(apkSignature.isValid()){
return apkSignature;
}
private boolean matchesSignature(byte[] bytes, int offset){
byte[] sig = APK_SIGNING_BLOCK_MAGIC;
int length = bytes.length - offset;
if (length<sig.length){
return false;
return null;
}
for(int i=0;i<sig.length;i++){
int j = offset+i;
if(sig[i] != bytes[j]){
return false;
}
if(bytes[j] ==0){
return false;
public void writeSigData(File dir) throws IOException{
for(ApkSignature apkSignature:mSignatures){
apkSignature.writeToDir(dir);
}
}
return true;
@Override
public String toString(){
return "size=" + getTotalSize() + ", count=" + getSignatures().size();
}
private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;

View File

@ -18,6 +18,7 @@ package com.reandroid.archive2.model;
import com.reandroid.archive2.block.CentralEntryHeader;
import com.reandroid.archive2.block.EndRecord;
import com.reandroid.archive2.block.LocalFileHeader;
import com.reandroid.archive2.block.SignatureFooter;
import com.reandroid.archive2.io.ZipSource;
import java.io.ByteArrayInputStream;
@ -30,6 +31,7 @@ import java.util.Objects;
public class CentralFileDirectory {
private final List<CentralEntryHeader> headerList;
private EndRecord endRecord;
private SignatureFooter signatureFooter;
public CentralFileDirectory(){
this.headerList = new ArrayList<>();
}
@ -64,20 +66,25 @@ public class CentralFileDirectory {
public List<CentralEntryHeader> getHeaderList() {
return headerList;
}
public SignatureFooter getSignatureFooter() {
return signatureFooter;
}
public EndRecord getEndRecord() {
return endRecord;
}
public void visit(ZipSource zipSource) throws IOException {
byte[] footer = zipSource.getFooter(EndRecord.MAX_LENGTH);
byte[] footer = zipSource.getFooter(SignatureFooter.MIN_SIZE + EndRecord.MAX_LENGTH);
EndRecord endRecord = findEndRecord(footer);
int length = (int) endRecord.getLengthOfCentralDirectory();
int endLength = endRecord.countBytes();
if(footer.length < (length + endLength)){
footer = zipSource.getFooter(length + endLength);
footer = zipSource.getFooter(SignatureFooter.MIN_SIZE + length + endLength);
}
int offset = footer.length - length - endLength;
this.endRecord = endRecord;
loadCentralFileHeaders(footer, offset, length);
this.signatureFooter = tryFindSignatureFooter(footer, endRecord);
}
private void loadCentralFileHeaders(byte[] footer, int offset, int length) throws IOException {
ByteArrayInputStream inputStream = new ByteArrayInputStream(footer, offset, length);
@ -107,4 +114,20 @@ public class CentralFileDirectory {
}
throw new IOException("Failed to find end record");
}
private SignatureFooter tryFindSignatureFooter(byte[] footer, EndRecord endRecord) throws IOException {
int lenCd = (int) endRecord.getLengthOfCentralDirectory();
int endLength = endRecord.countBytes();
int length = SignatureFooter.MIN_SIZE;
int offset = footer.length - endLength - lenCd - length;
if(offset < 0){
return null;
}
ByteArrayInputStream inputStream = new ByteArrayInputStream(footer, offset, length);
SignatureFooter signatureFooter = new SignatureFooter();
signatureFooter.readBytes(inputStream);
if(signatureFooter.isValid()){
return signatureFooter;
}
return null;
}
}

View File

@ -15,10 +15,7 @@
*/
package com.reandroid.archive2.model;
import com.reandroid.archive2.block.CentralEntryHeader;
import com.reandroid.archive2.block.DataDescriptor;
import com.reandroid.archive2.block.EndRecord;
import com.reandroid.archive2.block.LocalFileHeader;
import com.reandroid.archive2.block.*;
import com.reandroid.archive2.io.ZipSource;
import java.io.IOException;
@ -41,12 +38,12 @@ public class LocalFileDirectory {
public void visit(ZipSource zipSource) throws IOException {
getCentralFileDirectory().visit(zipSource);
visitLocalFile(zipSource);
visitApkSigBlock(zipSource);
}
private void visitLocalFile(ZipSource zipSource) throws IOException {
EndRecord endRecord = getCentralFileDirectory().getEndRecord();
InputStream inputStream = zipSource.getInputStream(0, endRecord.getOffsetOfCentralDirectory());
visitLocalFile(inputStream);
visitApkSigBlock(inputStream);
inputStream.close();
}
private void visitLocalFile(InputStream inputStream) throws IOException {
@ -82,19 +79,23 @@ public class LocalFileDirectory {
}
mTotalDataLength = offset;
}
private void visitApkSigBlock(InputStream inputStream) throws IOException{
int blockSize = (int) (getCentralFileDirectory().getEndRecord().getOffsetOfCentralDirectory()
- getTotalDataLength());
if(blockSize<=0){
private void visitApkSigBlock(ZipSource zipSource) throws IOException{
CentralFileDirectory cfd = getCentralFileDirectory();
SignatureFooter footer = cfd.getSignatureFooter();
if(footer == null || !footer.isValid()){
return;
}
byte[] bytes = new byte[blockSize];
inputStream.read(bytes, 0, bytes.length);
ApkSigBlock apkSigBlock = new ApkSigBlock();
apkSigBlock.parse(bytes);
EndRecord endRecord = cfd.getEndRecord();
long length = footer.getSigBlockSizeInFooter() + 8;
long offset = endRecord.getOffsetOfCentralDirectory()
- length;
ApkSigBlock apkSigBlock = new ApkSigBlock(footer);
apkSigBlock.readBytes(zipSource.getInputStream(offset, length));
this.apkSigBlock = apkSigBlock;
}
public ApkSigBlock getApkSigBlock() {
return apkSigBlock;
}
public CentralFileDirectory getCentralFileDirectory() {
return centralFileDirectory;
}