go-common/app/admin/main/macross/tools/apk.go
2019-04-22 18:49:16 +08:00

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
}