adb-toolkit/backend_apkaudit_purego.go

215 lines
6.2 KiB
Go

package main
// Pure-Go fallback parsers for the APK auditor, used when the Android SDK
// build-tools (aapt2 / apksigner) or a JDK are not installed. This makes the
// audit fully self-contained for users who only have adb/fastboot.
//
// Reference path (aapt2 + apksigner) stays the default when those tools are
// present; these functions are only invoked as a fallback.
import (
"bytes"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"encoding/xml"
"strings"
"time"
"github.com/avast/apkparser"
"github.com/avast/apkverifier"
)
// hasBuildTool reports whether a build-tool can be resolved on this machine.
func (a *App) hasBuildTool(name string) bool {
_, err := a.resolveBuildTool(name)
return err == nil
}
// parseManifestGo decodes the binary AndroidManifest.xml with apkparser and
// fills the same audit fields the aapt2 path would (minus the resource-resolved
// app label, which needs the resource table).
func parseManifestGo(path string, audit *APKAudit) {
var buf bytes.Buffer
enc := xml.NewEncoder(&buf)
enc.Indent("", " ")
zipErr, _, _ := apkparser.ParseApk(path, enc)
_ = enc.Flush()
if zipErr != nil {
return
}
audit.ManifestXML = buf.String()
dec := xml.NewDecoder(strings.NewReader(audit.ManifestXML))
compIdx := -1
allowBackupSeen, cleartextSeen := false, false
for {
tok, err := dec.Token()
if err != nil {
break
}
switch t := tok.(type) {
case xml.StartElement:
attr := func(k string) string {
for _, a := range t.Attr {
if a.Name.Local == k {
return a.Value
}
}
return ""
}
has := func(k string) bool {
for _, a := range t.Attr {
if a.Name.Local == k {
return true
}
}
return false
}
switch t.Name.Local {
case "manifest":
audit.PackageName = attr("package")
if v := attr("versionName"); v != "" {
audit.VersionName = v
}
if v := attr("versionCode"); v != "" {
audit.VersionCode = v
}
if v := attr("compileSdkVersion"); v != "" {
audit.CompileSDK = v
}
case "uses-sdk":
if v := attr("minSdkVersion"); v != "" {
audit.MinSDK = v
}
if v := attr("targetSdkVersion"); v != "" {
audit.TargetSDK = v
}
case "uses-permission", "uses-permission-sdk-23":
if n := attr("name"); n != "" {
audit.Permissions = append(audit.Permissions, Permission{Name: n, Dangerous: dangerousPermissions[n]})
}
case "application":
if has("debuggable") {
audit.Debuggable = isTrue(attr("debuggable"))
}
if has("allowBackup") {
allowBackupSeen = true
audit.AllowBackup = isTrue(attr("allowBackup"))
}
if has("usesCleartextTraffic") {
cleartextSeen = true
audit.UsesCleartext = isTrue(attr("usesCleartextTraffic"))
}
if has("networkSecurityConfig") {
audit.HasNSC = true
}
case "activity", "activity-alias", "service", "receiver", "provider":
typ := t.Name.Local
if typ == "activity-alias" {
typ = "activity"
}
c := Component{Type: typ, Name: attr("name"), Permission: attr("permission")}
if has("exported") {
c.Exported = isTrue(attr("exported"))
c.explicitExported = true
}
audit.Components = append(audit.Components, c)
compIdx = len(audit.Components) - 1
case "intent-filter":
if compIdx >= 0 {
audit.Components[compIdx].IntentFilters = append(audit.Components[compIdx].IntentFilters, "")
}
case "action", "category":
if n := attr("name"); n != "" && compIdx >= 0 && len(audit.Components[compIdx].IntentFilters) > 0 {
idx := len(audit.Components[compIdx].IntentFilters) - 1
sep := ""
if audit.Components[compIdx].IntentFilters[idx] != "" {
sep = ", "
}
audit.Components[compIdx].IntentFilters[idx] += sep + shortName(n)
}
}
case xml.EndElement:
switch t.Name.Local {
case "activity", "activity-alias", "service", "receiver", "provider":
compIdx = -1
}
}
}
// defaults the manifest may omit
if !allowBackupSeen {
audit.AllowBackup = true
}
if !cleartextSeen {
if v := atoiSafe(audit.TargetSDK); v > 0 && v < 28 {
audit.UsesCleartext = true
}
}
for i := range audit.Components {
c := &audit.Components[i]
if !c.Exported && !c.explicitExported && len(c.IntentFilters) > 0 {
c.ExportedImplicit = true
}
}
}
// parseCertGo extracts and verifies the signing certificate with apkverifier.
func parseCertGo(path string, audit *APKAudit) {
res, err := apkverifier.Verify(path, nil)
if err != nil {
audit.Cert.Error = firstLine(err.Error())
}
if len(res.SignerCerts) > 0 && len(res.SignerCerts[0]) > 0 {
leaf := res.SignerCerts[0][0]
audit.Cert.Verified = err == nil
audit.Cert.Subject = leaf.Subject.String()
audit.Cert.Issuer = leaf.Issuer.String()
audit.Cert.Serial = leaf.SerialNumber.String()
audit.Cert.SigAlgo = leaf.SignatureAlgorithm.String()
audit.Cert.ValidFrom = leaf.NotBefore.Format("2006-01-02")
audit.Cert.ValidTo = leaf.NotAfter.Format("2006-01-02")
s256 := sha256.Sum256(leaf.Raw)
audit.Cert.SHA256 = hex.EncodeToString(s256[:])
s1 := sha1.Sum(leaf.Raw)
audit.Cert.SHA1 = hex.EncodeToString(s1[:])
}
// Only claim a signing scheme when verification actually succeeded — this
// matches apksigner, which reports all-false for unsigned/broken APKs.
// (apkverifier otherwise defaults SchemeId to 1 even when nothing verifies.)
if err == nil {
switch {
case res.SigningSchemeId >= 3: // 3 or 3.1
audit.Cert.V3 = true
case res.SigningSchemeId == 2:
audit.Cert.V2 = true
case res.SigningSchemeId == 1:
audit.Cert.V1 = true
}
}
}
// finalizeCert derives debug/weak/expired flags from whichever path populated
// the cert fields, so both reference and fallback paths behave the same.
func finalizeCert(audit *APKAudit) {
subjIssuer := audit.Cert.Subject + " " + audit.Cert.Issuer
if strings.Contains(strings.ToLower(subjIssuer), "android debug") ||
strings.Contains(subjIssuer, "CN=Android Debug") {
audit.Cert.IsDebug = true
}
algoUp := strings.ToUpper(audit.Cert.SigAlgo)
if strings.Contains(algoUp, "MD5") || strings.Contains(algoUp, "SHA1") || strings.Contains(algoUp, "SHA-1") {
audit.Cert.WeakAlgo = true
}
if audit.Cert.ValidTo != "" {
if t, ok := parseCertTime(audit.Cert.ValidTo); ok && t.Before(time.Now()) {
audit.Cert.Expired = true
}
}
}