adb-toolkit/backend_bootinfo.go

137 lines
3.7 KiB
Go

package main
import (
"bytes"
"crypto/sha1"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"os"
)
// Boot-image analysis + file hashing — both work entirely on local files, no
// device required.
type BootInfo struct {
Valid bool `json:"valid"`
Type string `json:"type"`
HeaderVersion int `json:"headerVersion"`
AndroidVersion string `json:"androidVersion"`
SecurityPatch string `json:"securityPatch"`
PageSize int `json:"pageSize"`
KernelKB int `json:"kernelKB"`
RamdiskKB int `json:"ramdiskKB"`
SizeMB int `json:"sizeMB"`
SHA1 string `json:"sha1"`
SHA256 string `json:"sha256"`
Root string `json:"root"`
}
// AnalyzeBootImage parses an Android boot / init_boot / vendor_boot image:
// header version, OS version + security patch, sizes, hashes, and a best-effort
// scan for root-solution markers (Magisk / KernelSU / APatch).
func (a *App) AnalyzeBootImage(path string) (BootInfo, error) {
var bi BootInfo
st, err := os.Stat(path)
if err != nil {
return bi, err
}
if st.Size() > 256<<20 {
return bi, fmt.Errorf("file too large to be a boot image (%d MB)", st.Size()>>20)
}
data, err := os.ReadFile(path)
if err != nil {
return bi, err
}
if len(data) < 64 {
return bi, fmt.Errorf("file too small to be a boot image")
}
bi.SizeMB = len(data) / (1024 * 1024)
s1 := sha1.Sum(data)
bi.SHA1 = hex.EncodeToString(s1[:])
s2 := sha256.Sum256(data)
bi.SHA256 = hex.EncodeToString(s2[:])
switch string(data[0:8]) {
case "ANDROID!":
bi.Type = "boot / init_boot"
bi.Valid = true
case "VNDRBOOT":
bi.Type = "vendor_boot"
bi.Valid = true
default:
bi.Type = "not a boot image"
}
if bi.Valid && bi.Type != "vendor_boot" {
hv := binary.LittleEndian.Uint32(data[40:44])
bi.HeaderVersion = int(hv)
osverOff := 44
if hv >= 3 {
osverOff = 16
}
if osverOff+4 <= len(data) {
osver := binary.LittleEndian.Uint32(data[osverOff : osverOff+4])
if osver != 0 {
ver := osver >> 11
bi.AndroidVersion = fmt.Sprintf("%d.%d.%d", (ver>>14)&0x7f, (ver>>7)&0x7f, ver&0x7f)
patch := osver & 0x7ff
month := patch & 0xf
if month >= 1 && month <= 12 {
bi.SecurityPatch = fmt.Sprintf("%04d-%02d", 2000+((patch>>4)&0x7f), month)
}
}
}
if hv >= 3 {
bi.KernelKB = int(binary.LittleEndian.Uint32(data[8:12])) / 1024
bi.RamdiskKB = int(binary.LittleEndian.Uint32(data[12:16])) / 1024
bi.PageSize = 4096
} else {
bi.KernelKB = int(binary.LittleEndian.Uint32(data[8:12])) / 1024
bi.RamdiskKB = int(binary.LittleEndian.Uint32(data[16:20])) / 1024
bi.PageSize = int(binary.LittleEndian.Uint32(data[36:40]))
}
}
switch {
case bytes.Contains(data, []byte("KernelSU")) || bytes.Contains(data, []byte("ksud")):
bi.Root = "KernelSU markers found"
case bytes.Contains(data, []byte("APatch")) || bytes.Contains(data, []byte("apatch")):
bi.Root = "APatch markers found"
case bytes.Contains(data, []byte("MAGISK")) || bytes.Contains(data, []byte("magisk")):
bi.Root = "Magisk markers found"
default:
bi.Root = "none (appears stock)"
}
return bi, nil
}
type FileHashes struct {
SHA256 string `json:"sha256"`
SHA1 string `json:"sha1"`
SizeBytes int64 `json:"sizeBytes"`
}
// HashFile streams a file and returns its SHA-256 / SHA-1 (works for any size).
func (a *App) HashFile(path string) (FileHashes, error) {
f, err := os.Open(path)
if err != nil {
return FileHashes{}, err
}
defer f.Close()
st, _ := f.Stat()
h1, h2 := sha1.New(), sha256.New()
if _, err := io.Copy(io.MultiWriter(h1, h2), f); err != nil {
return FileHashes{}, err
}
return FileHashes{
SHA256: hex.EncodeToString(h2.Sum(nil)),
SHA1: hex.EncodeToString(h1.Sum(nil)),
SizeBytes: st.Size(),
}, nil
}