Skip to content

Commit

Permalink
feat: add --json flag to ipsw fw tc cmd + fix macOS img4 support
Browse files Browse the repository at this point in the history
  • Loading branch information
blacktop committed Oct 5, 2024
1 parent 182629f commit 1900247
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 21 deletions.
58 changes: 51 additions & 7 deletions cmd/ipsw/cmd/fw/tc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"))
}

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
},
}
77 changes: 63 additions & 14 deletions internal/commands/fw/trustcache.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"strings"

Expand Down Expand Up @@ -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
Expand All @@ -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(),
})
}

/*
Expand Down Expand Up @@ -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
}

Expand All @@ -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 {
Expand Down
21 changes: 21 additions & 0 deletions internal/magic/magic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions pkg/img4/img4.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 1900247

Please sign in to comment.