379 lines
11 KiB
Go
379 lines
11 KiB
Go
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
|
|
}
|