package resources import ( "crypto/sha1" "crypto/sha256" "encoding/hex" "errors" "fmt" "io" "os" "path/filepath" "regexp" "strings" "github.com/blacktop/go-macho" "github.com/blacktop/go-plist" pl "github.com/blacktop/ipsw/pkg/plist" ) const libraryDirectory = "Contents/Library" const resourcesDirectory = "Contents/Resources" const frameworksDirectory = "Contents/Frameworks" const CodeResourcesPath = "Contents/_CodeSignature/CodeResources" type hash2 struct { CDHash []byte `plist:"cdhash,omitempty" xml:"cdhash,omitempty"` Requirement string `plist:"requirement,omitempty" xml:"requirement,omitempty"` Hash2 []byte `plist:"hash2,omitempty" xml:"hash2,omitempty"` Symlink string `plist:"symlink,omitempty" xml:"symlink,omitempty"` Optional bool `plist:"optional,omitempty" xml:"optional,omitempty"` } type CodeResources struct { Files map[string][]byte `plist:"files,omitempty" xml:"files,omitempty"` Files2 map[string]hash2 `plist:"files2,omitempty" xml:"files2,omitempty"` Rules map[string]any `plist:"rules,omitempty" xml:"rules,omitempty"` Rules2 map[string]any `plist:"rules2,omitempty" xml:"rules2,omitempty"` } func CreateCodeResources(dir string) error { cr := &CodeResources{ Files: make(map[string][]byte), Files2: make(map[string]hash2), Rules: make(map[string]any), Rules2: make(map[string]any), } // rules cr.Rules["^Resources/"] = true cr.Rules["^Resources/.*\\.lproj/"] = struct { Optional bool `plist:"optional,omitempty" xml:"optional,omitempty"` Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"` }{ Optional: true, Weight: 1000, } cr.Rules["^Resources/.*\\.lproj/locversion.plist$"] = struct { Omit bool `plist:"omit,omitempty" xml:"omit,omitempty"` Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"` }{ Omit: true, Weight: 1100, } cr.Rules["^Resources/Base\\.lproj/"] = struct { Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"` }{ Weight: 1010, } cr.Rules["^version.plist$"] = true // rules2 cr.Rules2[".*\\.dSYM($|/)"] = struct { Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"` }{ Weight: 11, } cr.Rules2["^(.*/)?\\.DS_Store$"] = struct { Omit bool `plist:"omit,omitempty" xml:"omit,omitempty"` Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"` }{ Omit: true, Weight: 2000, } cr.Rules2["^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/"] = struct { Nested bool `plist:"nested,omitempty" xml:"nested,omitempty"` Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"` }{ Nested: true, Weight: 10, } cr.Rules2["^.*"] = true cr.Rules2["^Info\\.plist$"] = struct { Omit bool `plist:"omit,omitempty" xml:"omit,omitempty"` Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"` }{ Omit: true, Weight: 20, } cr.Rules2["^PkgInfo$"] = struct { Omit bool `plist:"omit,omitempty" xml:"omit,omitempty"` Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"` }{ Omit: true, Weight: 20, } cr.Rules2["^Resources/"] = struct { Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"` }{ Weight: 20, } cr.Rules2["^Resources/.*\\.lproj/"] = struct { Optional bool `plist:"optional,omitempty" xml:"optional,omitempty"` Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"` }{ Optional: true, Weight: 1000, } cr.Rules2["^Resources/.*\\.lproj/locversion.plist$"] = struct { Omit bool `plist:"omit,omitempty" xml:"omit,omitempty"` Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"` }{ Omit: true, Weight: 1100, } cr.Rules2["^Resources/Base\\.lproj/"] = struct { Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"` }{ Weight: 1010, } cr.Rules2["^[^/]+$"] = struct { Nested bool `plist:"nested,omitempty" xml:"nested,omitempty"` Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"` }{ Nested: true, Weight: 10, } cr.Rules2["^embedded\\.provisionprofile$"] = struct { Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"` }{ Weight: 20, } cr.Rules2["^version\\.plist$"] = struct { Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"` }{ Weight: 20, } if err := filepath.WalkDir(filepath.Join(dir, frameworksDirectory), func(path string, d os.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } relPath, err := filepath.Rel(filepath.Join(dir, "Contents"), path) if err != nil { return err } var fwpath string if regexp.MustCompile(`^Frameworks/[^/]+\.framework/.+Info\.plist$`).MatchString(relPath) { dat, err := os.ReadFile(path) if err != nil { return fmt.Errorf("failed to read %s: %w", path, err) } ainfo, err := pl.ParseAppInfo(dat) if err != nil { return fmt.Errorf("failed to parse %s: %w", path, err) } if ainfo.CFBundleExecutable != "" { before, _, ok := strings.Cut(path, ".framework/") if ok { fwpath = filepath.Join(before+".framework/", ainfo.CFBundleExecutable) } } } else { return nil } if _, ok := cr.Files2[fwpath]; ok { return nil // already added } // files var m *macho.File if fat, err := macho.OpenFat(fwpath); err == nil { // UNIVERSAL MACHO defer fat.Close() m = fat.Arches[len(fat.Arches)-1].File } else { // SINGLE MACHO ARCH if errors.Is(err, macho.ErrNotFat) { m, err = macho.Open(fwpath) if err != nil { return nil // return err } defer m.Close() } else { return nil // not a macho file } } cs := m.CodeSignature() if cs == nil { return fmt.Errorf("no code signature in %s", path) } if len(cs.CodeDirectories) == 0 { return fmt.Errorf("no code directory in %s", path) } cdhashBytes, err := hex.DecodeString(cs.CodeDirectories[len(cs.CodeDirectories)-1].CDHash) if err != nil { return err } var requirement string if len(cs.Requirements) > 0 { requirement = cs.Requirements[0].Detail } relPath, err = filepath.Rel(filepath.Join(dir, "Contents"), fwpath) if err != nil { return err } cr.Files2[filepath.Dir(relPath)] = hash2{ CDHash: cdhashBytes[:20], Requirement: requirement, } return nil }); err != nil { return fmt.Errorf("failed to walk %s: %w", filepath.Join(dir, "Resources"), err) } // TODO: walk pluginsDirectory ?? // TODO: walk xpcServicesDirectory ?? // TODO: walk helpersDirectory ?? if err := filepath.WalkDir(filepath.Join(dir, libraryDirectory), func(path string, d os.DirEntry, err error) error { if err != nil { return err } relPath, err := filepath.Rel(filepath.Join(dir, "Contents"), path) if err != nil { return err } fi, err := os.Lstat(path) if err != nil { return fmt.Errorf("file %s does not exist", path) } var symlink string if (fi.Mode() & os.ModeSymlink) != 0 { symlink, err = os.Readlink(path) if err != nil { return fmt.Errorf("failed to eval symlink %s: %w", path, err) } } if symlink != "" { cr.Files2[relPath] = hash2{Symlink: symlink} } else { if d.IsDir() { return nil } f, err := os.Open(path) if err != nil { return err } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return err } cr.Files2[relPath] = hash2{Hash2: h.Sum(nil)} } return nil }); err != nil { return fmt.Errorf("failed to walk %s: %w", filepath.Join(dir, "Resources"), err) } if err := filepath.WalkDir(filepath.Join(dir, resourcesDirectory), func(path string, d os.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } relPath, err := filepath.Rel(filepath.Join(dir, "Contents"), path) if err != nil { return err } // files f, err := os.Open(path) if err != nil { return err } h := sha1.New() if _, err := io.Copy(h, f); err != nil { return err } cr.Files[relPath] = h.Sum(nil) f.Seek(0, io.SeekStart) // rewind // files2 h = sha256.New() if _, err := io.Copy(h, f); err != nil { return err } cr.Files2[relPath] = hash2{Hash2: h.Sum(nil)} if err := f.Close(); err != nil { return err } return nil }); err != nil { return fmt.Errorf("failed to walk %s: %w", filepath.Join(dir, "Resources"), err) } if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, CodeResourcesPath)), 0755); err != nil { return fmt.Errorf("failed to create directory %s: %w", filepath.Dir(filepath.Join(dir, CodeResourcesPath)), err) } f, err := os.Create(filepath.Join(dir, CodeResourcesPath)) if err != nil { return fmt.Errorf("failed to create %s: %w", CodeResourcesPath, err) } defer f.Close() enc := plist.NewEncoder(f) enc.Indent("\t") return enc.Encode(cr) }