You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
378 lines
11 KiB
378 lines
11 KiB
package tools |
|
|
|
import ( |
|
"bytes" |
|
"encoding/json" |
|
"errors" |
|
"fmt" |
|
"math" |
|
"os" |
|
) |
|
|
|
const ( |
|
apkSigBlockMinSize uint32 = 32 |
|
|
|
// https://android.googlesource.com/platform/build/+/android-7.1.2_r27/tools/signapk/src/com/android/signapk/ApkSignerV2.java |
|
// APK_SIGNING_BLOCK_MAGIC = { |
|
// 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20, |
|
// 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32 } |
|
|
|
apkSigBlockMagicHi = 0x3234206b636f6c42 // LITTLE_ENDIAN, High |
|
apkSigBlockMagicLo = 0x20676953204b5041 // LITTLE_ENDIAN, Low |
|
apkChannelBlockID = 0x71777777 |
|
// https://en.wikipedia.org/wiki/Zip_(file_format) |
|
// https://android.googlesource.com/platform/build/+/android-7.1.2_r27/tools/signapk/src/com/android/signapk/ZipUtils.java |
|
zipEocdRecSig = 0x06054b50 |
|
zipEocdRecMinSize = 22 |
|
zipEocdCentralDirSizeFieldOffset = 12 |
|
zipEocdCentralDirOffsetFieldOffset = 16 |
|
zipEocdCommentLengthFieldOffset = 20 |
|
) |
|
|
|
// ChannelInfo for apk |
|
type ChannelInfo struct { |
|
Channel string |
|
Extras map[string]string |
|
raw []byte |
|
} |
|
|
|
// ChannelInfo to string |
|
func (c *ChannelInfo) String() string { |
|
b := c.Bytes() |
|
if b == nil { |
|
return "" |
|
} |
|
return string(b) |
|
} |
|
|
|
// Bytes for ChannelInfo to byte array |
|
func (c *ChannelInfo) Bytes() []byte { |
|
if c.raw != nil { |
|
return c.raw |
|
} |
|
if len(c.Channel) == 0 && c.Extras == nil { |
|
return nil |
|
} |
|
var buf bytes.Buffer |
|
buf.WriteByte('{') |
|
if len(c.Channel) != 0 { |
|
buf.WriteString("\"channel\":") |
|
buf.WriteByte('"') |
|
buf.WriteString(c.Channel) |
|
buf.WriteByte('"') |
|
buf.WriteByte(',') |
|
} |
|
|
|
if c.Extras != nil { |
|
for k, v := range c.Extras { |
|
buf.WriteByte('"') |
|
buf.WriteString(k) |
|
buf.WriteByte('"') |
|
buf.WriteByte(':') |
|
buf.WriteByte('"') |
|
buf.WriteString(v) |
|
buf.WriteByte('"') |
|
buf.WriteByte(',') |
|
} |
|
} |
|
if buf.Len() > 2 { |
|
buf.Truncate(buf.Len() - 1) |
|
} |
|
|
|
buf.WriteByte('}') |
|
|
|
return buf.Bytes() |
|
} |
|
|
|
func readChannelInfo(file string) (c ChannelInfo, err error) { |
|
block, err := readChannelBlock(file) |
|
if err != nil { |
|
return c, err |
|
} |
|
|
|
if block != nil { |
|
var bundle map[string]string |
|
err := json.Unmarshal(block, &bundle) |
|
if err != nil { |
|
return c, err |
|
} |
|
c.Channel = bundle["channel"] |
|
delete(bundle, "channel") |
|
c.Extras = bundle |
|
c.raw = block |
|
} |
|
return c, nil |
|
} |
|
|
|
// read block associated to apkChannelBlockID |
|
func readChannelBlock(file string) ([]byte, error) { |
|
m, err := readIDValues(file, apkChannelBlockID) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return m[apkChannelBlockID], nil |
|
} |
|
|
|
func readIDValues(file string, ids ...uint32) (map[uint32][]byte, error) { |
|
f, err := os.Open(file) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer f.Close() |
|
eocd, offset, err := findEndOfCentralDirectoryRecord(f) |
|
if err != nil { |
|
return nil, err |
|
} |
|
if offset <= 0 { |
|
return nil, errors.New("Cannot find EOCD record, maybe a broken zip file") |
|
} |
|
centralDirOffset := getEocdCentralDirectoryOffset(eocd) |
|
block, _, err := findApkSigningBlock(f, centralDirOffset) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return findIDValuesInApkSigningBlock(block, ids...) |
|
} |
|
|
|
// End of central directory record (EOCD) |
|
// |
|
// Offset Bytes Description[23] |
|
// 0 4 End of central directory signature = 0x06054b50 |
|
// 4 2 Number of this disk |
|
// 6 2 Disk where central directory starts |
|
// 8 2 Number of central directory records on this disk |
|
// 10 2 Total number of central directory records |
|
// 12 4 Size of central directory (bytes) |
|
// 16 4 Offset of start of central directory, relative to start of archive |
|
// 20 2 Comment length (n) |
|
// 22 n Comment |
|
// For a zip with no archive comment, the |
|
// end-of-central-directory record will be 22 bytes long, so |
|
// we expect to find the EOCD marker 22 bytes from the end. |
|
func findEndOfCentralDirectoryRecord(f *os.File) ([]byte, int64, error) { |
|
fi, err := f.Stat() |
|
if err != nil { |
|
return nil, -1, err |
|
} |
|
if fi.Size() < zipEocdRecMinSize { |
|
// No space for EoCD record in the file. |
|
return nil, -1, nil |
|
} |
|
// Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus |
|
// the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily |
|
// reading more data. |
|
ret, offset, err := findEOCDRecord(f, 0) |
|
if err != nil { |
|
return nil, -1, err |
|
} |
|
if ret != nil && offset != -1 { |
|
return ret, offset, nil |
|
} |
|
// EoCD does not start where we expected it to. Perhaps it contains a non-empty comment |
|
// field. Expand the search. The maximum size of the comment field in EoCD is 65535 because |
|
// the comment length field is an unsigned 16-bit number. |
|
return findEOCDRecord(f, math.MaxUint16) |
|
} |
|
|
|
func findEOCDRecord(f *os.File, maxCommentSize uint16) ([]byte, int64, error) { |
|
if maxCommentSize > math.MaxUint16 { |
|
return nil, -1, os.ErrInvalid |
|
} |
|
fi, err := f.Stat() |
|
if err != nil { |
|
return nil, -1, err |
|
} |
|
fileSize := fi.Size() |
|
if fileSize < zipEocdRecMinSize { |
|
// No space for EoCD record in the file. |
|
return nil, -1, nil |
|
} |
|
// Lower maxCommentSize if the file is too small. |
|
if s := uint16(fileSize - zipEocdRecMinSize); maxCommentSize > s { |
|
maxCommentSize = s |
|
} |
|
maxEocdSize := zipEocdRecMinSize + maxCommentSize |
|
bufOffsetInFile := fileSize - int64(maxEocdSize) |
|
buf := make([]byte, maxEocdSize) |
|
n, e := f.ReadAt(buf, bufOffsetInFile) |
|
if e != nil { |
|
return nil, -1, err |
|
} |
|
eocdOffsetInFile := |
|
func() int64 { |
|
eocdWithEmptyCommentStartPosition := n - zipEocdRecMinSize |
|
for expectedCommentLength := uint16(0); expectedCommentLength < maxCommentSize; expectedCommentLength++ { |
|
eocdStartPos := eocdWithEmptyCommentStartPosition - int(expectedCommentLength) |
|
if getUint32(buf, eocdStartPos) == zipEocdRecSig { |
|
n := eocdStartPos + zipEocdCommentLengthFieldOffset |
|
actualCommentLength := getUint16(buf, n) |
|
if actualCommentLength == expectedCommentLength { |
|
return int64(eocdStartPos) |
|
} |
|
} |
|
} |
|
return -1 |
|
}() |
|
if eocdOffsetInFile == -1 { |
|
// No EoCD record found in the buffer |
|
return nil, -1, nil |
|
} |
|
// EoCD found |
|
return buf[eocdOffsetInFile:], bufOffsetInFile + eocdOffsetInFile, nil |
|
|
|
} |
|
|
|
func getEocdCentralDirectoryOffset(buf []byte) uint32 { |
|
return getUint32(buf, zipEocdCentralDirOffsetFieldOffset) |
|
} |
|
func getEocdCentralDirectorySize(buf []byte) uint32 { |
|
return getUint32(buf, zipEocdCentralDirSizeFieldOffset) |
|
} |
|
|
|
func setEocdCentralDirectoryOffset(eocd []byte, offset uint32) { |
|
putUint32(offset, eocd, zipEocdCentralDirOffsetFieldOffset) |
|
} |
|
|
|
func isExpected(ids []uint32, test uint32) bool { |
|
for _, id := range ids { |
|
if id == test { |
|
return true |
|
} |
|
} |
|
return false |
|
} |
|
|
|
func findIDValuesInApkSigningBlock(block []byte, ids ...uint32) (map[uint32][]byte, error) { |
|
ret := make(map[uint32][]byte) |
|
position := 8 |
|
limit := len(block) - 24 |
|
entryCount := 0 |
|
for limit > position { // has remaining bytes |
|
entryCount++ |
|
if limit-position < 8 { // but not enough |
|
return nil, fmt.Errorf("APK Signing Block broken on entry #%d", entryCount) |
|
} |
|
length := int(getUint64(block, position)) |
|
position += 8 |
|
|
|
if length < 4 || length > limit-position { |
|
return nil, fmt.Errorf("APK Signing Block broken on entry #%d,"+ |
|
" size out of range: length=%d, remaining=%d", entryCount, length, limit-position) |
|
} |
|
nextEntryPosition := position + length |
|
id := getUint32(block, position) |
|
position += 4 |
|
if len(ids) == 0 || isExpected(ids, id) { |
|
ret[id] = block[position : position+length-4] |
|
} |
|
position = nextEntryPosition |
|
|
|
} |
|
return ret, nil |
|
} |
|
|
|
// Find the APK Signing Block. The block immediately precedes the Central Directory. |
|
// |
|
// FORMAT: |
|
// uint64: size (excluding this field) |
|
// repeated ID-value pairs: |
|
// uint64: size (excluding this field) |
|
// uint32: ID |
|
// (size - 4) bytes: value |
|
// uint64: size (same as the one above) |
|
// uint128: magic |
|
func findApkSigningBlock(f *os.File, centralDirOffset uint32) (block []byte, offset int64, err error) { |
|
|
|
if centralDirOffset < apkSigBlockMinSize { |
|
return block, offset, fmt.Errorf("APK too small for APK Signing Block."+ |
|
" ZIP Central Directory offset: %d", centralDirOffset) |
|
} |
|
// Read the footer of APK signing block |
|
// 24 = sizeof(uint128) + sizeof(uint64) |
|
footer := make([]byte, 24) |
|
_, err = f.ReadAt(footer, int64(centralDirOffset-24)) |
|
if err != nil { |
|
return |
|
} |
|
// Read the magic and block size |
|
var blockSizeInFooter = getUint64(footer, 0) |
|
if blockSizeInFooter < 24 || blockSizeInFooter > uint64(math.MaxInt32-8 /* ID-value size field*/) { |
|
return block, offset, fmt.Errorf("APK Signing Block size out of range: %d", blockSizeInFooter) |
|
} |
|
if getUint64(footer, 8) != apkSigBlockMagicLo || |
|
getUint64(footer, 16) != apkSigBlockMagicHi { |
|
return block, offset, errors.New("No APK Signing Block before ZIP Central Directory") |
|
} |
|
|
|
totalSize := blockSizeInFooter + 8 /* APK signing block size field*/ |
|
|
|
offset = int64(uint64(centralDirOffset) - totalSize) |
|
|
|
if offset <= 0 { |
|
return block, offset, fmt.Errorf("invalid offset for APK Signing Block %d", offset) |
|
} |
|
block = make([]byte, totalSize) |
|
_, err = f.ReadAt(block, offset) |
|
if err != nil { |
|
return |
|
} |
|
blockSizeInHeader := getUint64(block, 0) |
|
if blockSizeInHeader != blockSizeInFooter { |
|
return nil, offset, fmt.Errorf("APK Signing Block sizes in header "+ |
|
"and footer are mismatched! Except %d but %d", blockSizeInFooter, blockSizeInHeader) |
|
} |
|
|
|
return block, offset, nil |
|
} |
|
|
|
// FORMAT: |
|
// uint64: size (excluding this field) |
|
// repeated ID-value pairs: |
|
// uint64: size (excluding this field) |
|
// uint32: ID |
|
// (size - 4) bytes: value |
|
// uint64: size (same as the one above) |
|
// uint128: magic |
|
func makeSigningBlockWithChannelInfo(info ChannelInfo, signingBlock []byte) ([]byte, int, error) { |
|
|
|
signingBlockSize := getUint64(signingBlock, 0) |
|
signingBlockLen := len(signingBlock) |
|
if n := uint64(signingBlockLen - 8); signingBlockSize != n { |
|
return nil, 0, fmt.Errorf("APK Signing Block is illegal! Expect size %d but %d", signingBlockSize, n) |
|
} |
|
|
|
channelValue := info.Bytes() |
|
channelValueSize := uint64(4 + len(channelValue)) |
|
resultSize := 8 + signingBlockSize + 8 + channelValueSize |
|
|
|
newBlock := make([]byte, resultSize) |
|
position := 0 |
|
putUint64(resultSize-8, newBlock, position) |
|
position += 8 |
|
// copy raw id-value pairs |
|
n, _ := copyBytes(signingBlock, 8, newBlock, position, int(signingBlockSize)-16-8) |
|
position += n |
|
putUint64(channelValueSize, newBlock, position) |
|
position += 8 |
|
putUint32(apkChannelBlockID, newBlock, position) |
|
position += 4 |
|
n, _ = copyBytes(channelValue, 0, newBlock, position, len(channelValue)) |
|
position += n |
|
|
|
putUint64(resultSize-8, newBlock, position) |
|
position += 8 |
|
copyBytes(signingBlock, signingBlockLen-16, newBlock, int(resultSize-16), 16) |
|
position += 16 |
|
|
|
if position != int(resultSize) { |
|
return nil, -1, fmt.Errorf("count mismatched ! %d vs %d", position, resultSize) |
|
} |
|
return newBlock, int(resultSize) - signingBlockLen, nil |
|
} |
|
|
|
func makeEocd(origin []byte, newCentralDirOffset uint32) []byte { |
|
eocd := make([]byte, len(origin)) |
|
copy(eocd, origin) |
|
setEocdCentralDirectoryOffset(eocd, newCentralDirOffset) |
|
return eocd |
|
}
|
|
|