From 190024704e25f34ef04bc236fc6b441926e15f8b Mon Sep 17 00:00:00 2001 From: blacktop Date: Sat, 5 Oct 2024 13:30:31 -0600 Subject: [PATCH] feat: add `--json` flag to `ipsw fw tc` cmd + fix macOS img4 support --- cmd/ipsw/cmd/fw/tc.go | 58 +++++++++++++++++++--- internal/commands/fw/trustcache.go | 77 ++++++++++++++++++++++++------ internal/magic/magic.go | 21 ++++++++ pkg/img4/img4.go | 9 ++++ 4 files changed, 144 insertions(+), 21 deletions(-) diff --git a/cmd/ipsw/cmd/fw/tc.go b/cmd/ipsw/cmd/fw/tc.go index 7d114455f..f469d426a 100644 --- a/cmd/ipsw/cmd/fw/tc.go +++ b/cmd/ipsw/cmd/fw/tc.go @@ -22,8 +22,11 @@ THE SOFTWARE. package fw import ( + "encoding/json" "fmt" + "os" "path/filepath" + "strings" "github.com/apex/log" "github.com/blacktop/ipsw/internal/commands/extract" @@ -41,8 +44,10 @@ import ( func init() { FwCmd.AddCommand(tcCmd) + tcCmd.Flags().BoolP("json", "j", false, "Output in JSON format") tcCmd.Flags().StringP("output", "o", "", "Folder to extract files to") tcCmd.MarkFlagDirname("output") + viper.BindPFlag("fw.tc.json", tcCmd.Flags().Lookup("json")) viper.BindPFlag("fw.tc.output", tcCmd.Flags().Lookup("output")) } @@ -57,20 +62,33 @@ var tcCmd = &cobra.Command{ log.SetLevel(log.DebugLevel) } + tcs := make(map[string]*fwcmd.TrustCache) + if isZip, err := magic.IsZip(filepath.Clean(args[0])); err != nil { return fmt.Errorf("failed to determine if file is a zip: %v", err) } else if isZip { out, err := extract.Search(&extract.Config{ IPSW: filepath.Clean(args[0]), Pattern: ".trustcache$", - Output: viper.GetString("fw.tc.output"), + Output: os.TempDir(), }) if err != nil { return err } for _, f := range out { - if ok, _ := magic.IsIm4p(f); ok { - log.WithField("file", f).Info("Processing IM4P file") + if ok, _ := magic.IsImg4(f); ok { + log.WithField("file", f).Debug("Processing IMG4 file") + img4, err := img4.OpenImg4(f) + if err != nil { + return fmt.Errorf("failed to open img4: %v", err) + } + tc, err := fwcmd.ParseTrustCache(img4.IM4P.Data) + if err != nil { + return fmt.Errorf("failed to parse trust cache: %v", err) + } + tcs[f] = tc + } else if ok, _ := magic.IsIm4p(f); ok { + log.WithField("file", f).Debug("Processing IM4P file") im4p, err := img4.OpenIm4p(f) if err != nil { return err @@ -79,14 +97,18 @@ var tcCmd = &cobra.Command{ if err != nil { return fmt.Errorf("failed to parse trust cache: %v", err) } - fmt.Println(tc) + tcs[strings.TrimPrefix(f, os.TempDir())] = tc } else { - return fmt.Errorf("unsupported file type: expected IM4P") + return fmt.Errorf("unsupported file type: expected IMG4/IM4P: %s", f) } } + // cleanup + for _, f := range out { + os.Remove(f) + } } else { if ok, _ := magic.IsIm4p(filepath.Clean(args[0])); ok { - log.WithField("file", filepath.Clean(args[0])).Info("Processing IM4P file") + log.WithField("file", filepath.Clean(args[0])).Debug("Processing IM4P file") im4p, err := img4.OpenIm4p(filepath.Clean(args[0])) if err != nil { return err @@ -95,12 +117,34 @@ var tcCmd = &cobra.Command{ if err != nil { return fmt.Errorf("failed to parse trust cache: %v", err) } - fmt.Println(tc) + tcs[filepath.Clean(args[0])] = tc } else { return fmt.Errorf("unsupported file type: expected IM4P") } } + if viper.GetBool("fw.tc.json") { + dat, err := json.Marshal(tcs) + if err != nil { + return err + } + if viper.IsSet("fw.tc.output") { + dat, err := json.Marshal(tcs) + if err != nil { + return err + } + log.Info("Creating JSON trustcache info file: " + filepath.Join(viper.GetString("fw.tc.output"), "trustcache.json")) + os.WriteFile(filepath.Join(viper.GetString("fw.tc.output"), "trustcache.json"), dat, 0644) + } else { + fmt.Println(string(dat)) + } + } else { + for file, tc := range tcs { + log.Info(file) + fmt.Println(tc) + } + } + return nil }, } diff --git a/internal/commands/fw/trustcache.go b/internal/commands/fw/trustcache.go index 7fcb74c9d..86faedeea 100644 --- a/internal/commands/fw/trustcache.go +++ b/internal/commands/fw/trustcache.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/binary" "encoding/hex" + "encoding/json" "fmt" "strings" @@ -68,18 +69,32 @@ func (c tcFlags) String() string { if len(flags) == 0 { return "" } - return fmt.Sprintf(" flags=%s", strings.Join(flags, "|")) + return strings.Join(flags, "|") } type TrustCache struct { TCHeader - Entries []any + Entries []any `json:"entries,omitempty"` +} + +func (tc TrustCache) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Version uint32 `json:"version,omitempty"` + UUID string `json:"uuid,omitempty"` + NumEntries uint32 `json:"num_entries,omitempty"` + Entries []any `json:"entries,omitempty"` + }{ + Version: tc.Version, + UUID: tc.UUID.String(), + NumEntries: tc.NumEntries, + Entries: tc.Entries, + }) } type TCHeader struct { - Version uint32 - UUID types.UUID - NumEntries uint32 + Version uint32 `json:"version,omitempty"` + UUID types.UUID `json:"uuid,omitempty"` + NumEntries uint32 `json:"num_entries,omitempty"` } type CDHash [CDHASH_LEN]byte @@ -89,13 +104,29 @@ func (c CDHash) String() string { } type TrustCacheEntryV1 struct { - CDHash CDHash - HashType hashType - Flags tcFlags + CDHash CDHash `json:"cdhash,omitempty"` + HashType hashType `json:"hash_type,omitempty"` + Flags tcFlags `json:"flags,omitempty"` } func (tc TrustCacheEntryV1) String() string { - return fmt.Sprintf("%s %s%s", tc.CDHash, tc.HashType, tc.Flags) + var flags string + if tc.Flags != 0 { + flags = fmt.Sprintf(" flags=%s", tc.Flags) + } + return fmt.Sprintf("%s %s%s", tc.CDHash, tc.HashType, flags) +} + +func (tc TrustCacheEntryV1) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + CDHash string `json:"cdhash,omitempty"` + HashType string `json:"hash_type,omitempty"` + Flags string `json:"flags,omitempty"` + }{ + CDHash: tc.CDHash.String(), + HashType: tc.HashType.String(), + Flags: tc.Flags.String(), + }) } /* @@ -132,10 +163,10 @@ Constraint Categories (from TrustCache, new in version 2): Self Constraint: validation-category == 1 */ type TrustCacheEntryV2 struct { - CDHash CDHash - HashType hashType - Flags tcFlags - ConstraintCategory uint8 + CDHash CDHash `json:"cdhash,omitempty"` + HashType hashType `json:"hash_type,omitempty"` + Flags tcFlags `json:"flags,omitempty"` + ConstraintCategory uint8 `json:"constraint_category,omitempty"` _ uint8 } @@ -144,7 +175,25 @@ func (tc TrustCacheEntryV2) String() string { if tc.ConstraintCategory != 0 { cat = fmt.Sprintf(" category=%d", tc.ConstraintCategory) } - return fmt.Sprintf("%s %s%s%s", tc.CDHash, tc.HashType, cat, tc.Flags) + var flags string + if tc.Flags != 0 { + flags = fmt.Sprintf(" flags=%s", tc.Flags) + } + return fmt.Sprintf("%s %s%s%s", tc.CDHash, tc.HashType, cat, flags) +} + +func (tc TrustCacheEntryV2) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + CDHash string `json:"cdhash,omitempty"` + HashType string `json:"hash_type,omitempty"` + Flags string `json:"flags,omitempty"` + ConstraintCategory uint8 `json:"constraint_category,omitempty"` + }{ + CDHash: tc.CDHash.String(), + HashType: tc.HashType.String(), + Flags: tc.Flags.String(), + ConstraintCategory: tc.ConstraintCategory, + }) } func (tc TrustCache) String() string { diff --git a/internal/magic/magic.go b/internal/magic/magic.go index 516b18b49..2959a8b40 100644 --- a/internal/magic/magic.go +++ b/internal/magic/magic.go @@ -96,6 +96,27 @@ type Asn1Header struct { Name string `asn1:"ia5"` // IM4P } +func IsImg4(filePath string) (bool, error) { + if filepath.Ext(filePath) == ".img4" { + return true, nil + } + + data, err := os.ReadFile(filePath) + if err != nil { + return false, fmt.Errorf("failed to read file %s: %w", filePath, err) + } + + var hdr Asn1Header + if _, err := asn1.Unmarshal(data, &hdr); err != nil { + return false, fmt.Errorf("failed to ASN.1 parse header: %v", err) + } + + if hdr.Name == "IMG4" { + return true, nil + } + return false, nil +} + func IsIm4p(filePath string) (bool, error) { if filepath.Ext(filePath) == ".im4p" { return true, nil diff --git a/pkg/img4/img4.go b/pkg/img4/img4.go index 4a73a7ac6..703e97937 100644 --- a/pkg/img4/img4.go +++ b/pkg/img4/img4.go @@ -433,6 +433,15 @@ func ParseIm4p(r io.Reader) (*Im4p, error) { return &i, nil } +func OpenImg4(path string) (*img4, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + return ParseImg4(f) +} + func ParseImg4(r io.Reader) (*img4, error) { data := new(bytes.Buffer)