mirror of
https://github.com/blacktop/ipsw.git
synced 2026-06-07 12:27:36 +00:00
- Add `otaFilenameCandidates` to strip all known extensions (.aea, .ota, .zip) and generate a fallback candidate from the last underscore-delimited segment (e.g. downloaded filenames like `iPhone18,1_23F5043k_<hash>.aea` now match DB entries keyed by `<hash>`) - Add test covering the downloaded-filename → archive-stem lookup - Serialize `GetPayloadFiles` and `PayloadFiles` loops (remove `errgroup` concurrency) to fix data races and ensure correct error propagation with explicit resource cleanup - Add `ErrCryptexNotFound` sentinel error for programmatic detection - Add `system-arm64e` and `system-x86_64h` as explicit cryptex selector variants
960 lines
24 KiB
Go
960 lines
24 KiB
Go
package ota
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/apex/log"
|
|
"github.com/blacktop/ipsw/internal/magic"
|
|
"github.com/blacktop/ipsw/internal/utils"
|
|
"github.com/blacktop/ipsw/pkg/aea"
|
|
"github.com/blacktop/ipsw/pkg/bom"
|
|
"github.com/blacktop/ipsw/pkg/info"
|
|
"github.com/blacktop/ipsw/pkg/ota/pbzx"
|
|
"github.com/blacktop/ipsw/pkg/ota/ridiff"
|
|
"github.com/blacktop/ipsw/pkg/ota/yaa"
|
|
"github.com/dustin/go-humanize"
|
|
"golang.org/x/exp/maps"
|
|
"golang.org/x/sys/execabs"
|
|
)
|
|
|
|
// ErrCryptexNotFound is returned by ExtractCryptex when the OTA
|
|
// does not contain a matching cryptex member.
|
|
var ErrCryptexNotFound = errors.New("cryptex not found")
|
|
|
|
var (
|
|
reOTADeviceTreeIm4p = regexp.MustCompile(`.*DeviceTree.*im4p$`)
|
|
reOTAInfoPlist = regexp.MustCompile(`^Info\.plist$`)
|
|
reOTAAssetDataInfo = regexp.MustCompile(`^AssetData/Info\.plist$`)
|
|
reOTARestorePlist = regexp.MustCompile(`Restore\.plist$`)
|
|
reOTABuildManifest = regexp.MustCompile(`BuildManifest\.plist$`)
|
|
reOTASystemVersion = regexp.MustCompile(`SystemVersion\.plist$`)
|
|
)
|
|
|
|
type File struct {
|
|
name string
|
|
isDir bool
|
|
isDup bool
|
|
|
|
entry *yaa.Entry
|
|
zfile *zip.File
|
|
}
|
|
|
|
// A Reader serves content from a Apple Archive.
|
|
type Reader struct {
|
|
r io.ReaderAt
|
|
zr *zip.Reader
|
|
|
|
zfiles []*zip.File
|
|
yaa *yaa.YAA
|
|
|
|
isZip bool
|
|
|
|
// fileList is a list of files sorted by ename,
|
|
// for use by the Open method.
|
|
fileListOnce sync.Once
|
|
fileList []*File
|
|
bomFiles []fs.FileInfo
|
|
|
|
payloadMapOnce sync.Once
|
|
payloadMap map[string]string
|
|
}
|
|
|
|
type AA struct {
|
|
f *os.File
|
|
Reader
|
|
}
|
|
|
|
// Config holds optional configuration for opening OTA files
|
|
type Config struct {
|
|
// SymmetricKey is the base64-encoded AEA symmetric encryption key
|
|
SymmetricKey string
|
|
// Proxy to use when fetching AEA keys
|
|
Proxy string
|
|
// Insecure allows insecure connections when fetching AEA keys
|
|
Insecure bool
|
|
}
|
|
|
|
func getKeyFromName(name string) (string, error) {
|
|
_, rest, ok := strings.Cut(name, "[")
|
|
if !ok {
|
|
return "", fmt.Errorf("begining of KEY '[' not found in '%s'", name)
|
|
}
|
|
key, _, ok := strings.Cut(rest, "]")
|
|
if !ok {
|
|
return "", fmt.Errorf("end of KEY ']' not found in '%s'", name)
|
|
}
|
|
key = strings.ReplaceAll(key, "-", "+")
|
|
key = strings.ReplaceAll(key, "_", "/")
|
|
return key, nil
|
|
}
|
|
|
|
func NewOTA(r io.ReaderAt, size int64) (*AA, error) {
|
|
var err error
|
|
f := new(AA)
|
|
if f.isZip, err = magic.IsZipData(io.NewSectionReader(r, 0, 4)); err != nil {
|
|
return nil, err
|
|
} else if f.isZip { // check if file is a zip
|
|
f.isZip = true
|
|
if err := f.initZip(r, size); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
if err := f.init(r, size); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// Open opens an OTA file with optional configuration
|
|
// If conf is nil, default configuration is used (automatic key lookup)
|
|
func Open(name string, conf *Config) (*AA, error) {
|
|
// Use default config if nil
|
|
if conf == nil {
|
|
conf = &Config{}
|
|
}
|
|
|
|
if isAEA, err := magic.IsAEA(name); err != nil {
|
|
return nil, err
|
|
} else if isAEA { // check if file is AEA encrypted
|
|
var key string
|
|
if conf.SymmetricKey != "" {
|
|
key = conf.SymmetricKey
|
|
} else {
|
|
// Try to get key from filename (legacy behavior)
|
|
if keyFromName, err := getKeyFromName(name); err != nil {
|
|
// No key in filename - explicitly set empty to trigger automatic lookup
|
|
key = ""
|
|
log.Debug("No key in filename, will attempt automatic key lookup from AEA metadata")
|
|
} else {
|
|
key = keyFromName
|
|
}
|
|
}
|
|
// Call aea.Decrypt - if key is empty, it will attempt automatic lookup from AEA metadata
|
|
name, err = aea.Decrypt(&aea.DecryptConfig{
|
|
Input: name,
|
|
Output: os.TempDir(),
|
|
B64SymKey: key,
|
|
Proxy: conf.Proxy,
|
|
Insecure: conf.Insecure,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decrypt AEA: %v (try providing --key-val, --key-db, or ensure you're online for automatic key lookup)", err)
|
|
}
|
|
defer os.Remove(name)
|
|
}
|
|
f, err := os.Open(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fi, err := f.Stat()
|
|
if err != nil {
|
|
f.Close()
|
|
return nil, err
|
|
}
|
|
r := new(AA)
|
|
if r.isZip, err = magic.IsZip(name); err != nil {
|
|
return nil, err
|
|
} else if r.isZip { // check if file is a zip
|
|
r.isZip = true
|
|
if err = r.initZip(f, fi.Size()); err != nil {
|
|
f.Close()
|
|
return nil, err
|
|
}
|
|
} else {
|
|
if err = r.init(f, fi.Size()); err != nil {
|
|
f.Close()
|
|
return nil, err
|
|
}
|
|
}
|
|
r.f = f
|
|
return r, err
|
|
}
|
|
|
|
func (a *AA) Info() (*info.Info, error) {
|
|
var pfiles []fs.File
|
|
for _, file := range a.Files() {
|
|
switch {
|
|
case reOTADeviceTreeIm4p.MatchString(file.Name()):
|
|
fallthrough
|
|
case reOTAInfoPlist.MatchString(file.Name()):
|
|
fallthrough
|
|
case reOTAAssetDataInfo.MatchString(file.Name()):
|
|
fallthrough
|
|
case reOTARestorePlist.MatchString(file.Name()):
|
|
fallthrough
|
|
case reOTABuildManifest.MatchString(file.Name()):
|
|
fallthrough
|
|
case reOTASystemVersion.MatchString(file.Name()):
|
|
f, err := a.Open(file.Name(), true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
pfiles = append(pfiles, f)
|
|
}
|
|
}
|
|
if len(pfiles) == 0 {
|
|
return nil, fmt.Errorf("no plist files found")
|
|
}
|
|
return info.ParseOTAFiles(pfiles)
|
|
}
|
|
|
|
// Close closes the AA file, rendering it unusable for I/O.
|
|
func (a *AA) Close() error {
|
|
return a.f.Close()
|
|
}
|
|
|
|
func (r *Reader) initZip(rdr io.ReaderAt, size int64) (err error) {
|
|
r.zr, err = zip.NewReader(rdr, size)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.zfiles = r.zr.File
|
|
return nil
|
|
}
|
|
|
|
func (r *Reader) init(rdr io.ReaderAt, size int64) (err error) {
|
|
r.r = rdr
|
|
r.yaa = &yaa.YAA{}
|
|
rs := io.NewSectionReader(rdr, 0, size)
|
|
if err := r.yaa.Parse(rs); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// toValidName coerces name to be a valid name for fs.FS.Open.
|
|
func toValidName(name string) string {
|
|
name = strings.ReplaceAll(name, `\`, `/`)
|
|
p := path.Clean(name)
|
|
p = strings.TrimPrefix(p, "/")
|
|
for strings.HasPrefix(p, "../") {
|
|
p = p[len("../"):]
|
|
}
|
|
return p
|
|
}
|
|
|
|
func (r *Reader) initFileList() (ferr error) {
|
|
r.fileListOnce.Do(func() {
|
|
// files and knownDirs map from a file/directory name
|
|
// to an index into the r.fileList entry that we are
|
|
// building. They are used to mark duplicate entries.
|
|
files := make(map[string]int)
|
|
knownDirs := make(map[string]int)
|
|
|
|
// dirs[name] is true if name is known to be a directory,
|
|
// because it appears as a prefix in a path.
|
|
dirs := make(map[string]bool)
|
|
|
|
for _, file := range r.zfiles {
|
|
isDir := len(file.Name) > 0 && file.Name[len(file.Name)-1] == '/'
|
|
name := toValidName(file.Name)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
|
|
if idx, ok := files[name]; ok {
|
|
r.fileList[idx].isDup = true
|
|
continue
|
|
}
|
|
if idx, ok := knownDirs[name]; ok {
|
|
r.fileList[idx].isDup = true
|
|
continue
|
|
}
|
|
|
|
for dir := path.Dir(name); dir != "."; dir = path.Dir(dir) {
|
|
dirs[dir] = true
|
|
}
|
|
|
|
idx := len(r.fileList)
|
|
entry := &File{
|
|
name: name,
|
|
zfile: file,
|
|
isDir: isDir,
|
|
}
|
|
r.fileList = append(r.fileList, entry)
|
|
if isDir {
|
|
knownDirs[name] = idx
|
|
} else {
|
|
files[name] = idx
|
|
}
|
|
if strings.EqualFold(filepath.Base(file.Name), "post.bom") {
|
|
zr, err := file.Open()
|
|
if err != nil {
|
|
ferr = err
|
|
return
|
|
}
|
|
bdata, err := io.ReadAll(zr)
|
|
if err != nil {
|
|
ferr = err
|
|
return
|
|
}
|
|
bom, err := bom.New(bytes.NewReader(bdata))
|
|
if err != nil {
|
|
ferr = err
|
|
return
|
|
}
|
|
bfiles, err := bom.GetPaths()
|
|
if err != nil {
|
|
ferr = err
|
|
return
|
|
}
|
|
r.bomFiles = bfiles
|
|
}
|
|
}
|
|
for dir := range dirs {
|
|
if _, ok := knownDirs[dir]; !ok {
|
|
if idx, ok := files[dir]; ok {
|
|
r.fileList[idx].isDup = true
|
|
} else {
|
|
entry := &File{
|
|
name: dir,
|
|
zfile: nil,
|
|
isDir: true,
|
|
}
|
|
r.fileList = append(r.fileList, entry)
|
|
}
|
|
}
|
|
}
|
|
if r.yaa != nil {
|
|
for _, entry := range r.yaa.Entries {
|
|
if entry.Type != yaa.RegularFile && entry.Type != yaa.Directory {
|
|
continue
|
|
}
|
|
name := toValidName(entry.Path)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
if idx, ok := files[name]; ok {
|
|
r.fileList[idx].isDup = true
|
|
continue
|
|
}
|
|
if idx, ok := knownDirs[name]; ok {
|
|
r.fileList[idx].isDup = true
|
|
continue
|
|
}
|
|
for dir := path.Dir(name); dir != "."; dir = path.Dir(dir) {
|
|
dirs[dir] = true
|
|
}
|
|
idx := len(r.fileList)
|
|
entry := &File{
|
|
name: name,
|
|
entry: entry,
|
|
isDir: entry.IsDir(),
|
|
}
|
|
r.fileList = append(r.fileList, entry)
|
|
if entry.IsDir() {
|
|
knownDirs[name] = idx
|
|
} else {
|
|
files[name] = idx
|
|
}
|
|
}
|
|
// add BOM files
|
|
bomFiles, err := r.yaa.PostBOM()
|
|
if err != nil {
|
|
if !errors.Is(err, yaa.ErrPostBomNotFound) {
|
|
ferr = err
|
|
return
|
|
}
|
|
} else {
|
|
r.bomFiles = bomFiles
|
|
}
|
|
}
|
|
|
|
sort.Slice(r.fileList, func(i, j int) bool { return fileEntryLess(r.fileList[i].name, r.fileList[j].name) })
|
|
})
|
|
|
|
return ferr
|
|
}
|
|
|
|
func (r *Reader) initPayloadMap() (perr error) {
|
|
r.payloadMapOnce.Do(func() {
|
|
pre := regexp.MustCompile(`^payload.\d+$`)
|
|
r.payloadMap = make(map[string]string)
|
|
hdr := make([]byte, binary.Size(pbzx.Header{}))
|
|
var pbuf bytes.Buffer
|
|
for _, file := range r.Files() {
|
|
if file.isDir {
|
|
continue
|
|
}
|
|
if pre.MatchString(file.Base()) {
|
|
f, err := r.Open(file.Name(), false)
|
|
if err != nil {
|
|
perr = err
|
|
return
|
|
}
|
|
defer f.Close()
|
|
var header pbzx.Header
|
|
if err := binary.Read(f, binary.BigEndian, &header); err != nil {
|
|
perr = fmt.Errorf("failed to read pbzx header: %v", err)
|
|
return
|
|
}
|
|
if err := binary.Write(bytes.NewBuffer(hdr[:0]), binary.BigEndian, &header); err != nil {
|
|
perr = fmt.Errorf("failed to write pbzx header: %v", err)
|
|
return
|
|
}
|
|
cache := make([]byte, header.DeflateSize)
|
|
if _, err := f.Read(cache); err != nil {
|
|
perr = fmt.Errorf("failed to read pbzx block: %v", err)
|
|
return
|
|
}
|
|
block := make([]byte, len(hdr)+int(header.DeflateSize))
|
|
copy(block, hdr)
|
|
copy(block[len(hdr):], cache)
|
|
if err := pbzx.Extract(context.Background(), bytes.NewReader(block), &pbuf, runtime.NumCPU()); err != nil {
|
|
perr = err
|
|
return
|
|
}
|
|
aa := &yaa.YAA{}
|
|
if err := aa.Parse(bytes.NewReader(pbuf.Bytes())); err != nil {
|
|
if !errors.Is(err, io.ErrUnexpectedEOF) {
|
|
perr = fmt.Errorf("failed to parse payload: %v", err)
|
|
return
|
|
}
|
|
}
|
|
for _, entry := range aa.Entries {
|
|
if entry.Type == yaa.RegularFile && entry.Path != "" && entry.Size > 0 {
|
|
r.payloadMap[file.Name()] = entry.Path
|
|
pbuf.Reset()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
func fileEntryLess(x, y string) bool {
|
|
xdir, xelem, _ := split(x)
|
|
ydir, yelem, _ := split(y)
|
|
return xdir < ydir || xdir == ydir && xelem < yelem
|
|
}
|
|
|
|
func (r *Reader) Files() []*File {
|
|
r.initFileList()
|
|
return r.fileList
|
|
}
|
|
|
|
func (r *Reader) PostFiles() []fs.FileInfo {
|
|
r.initFileList()
|
|
return r.bomFiles
|
|
}
|
|
|
|
func (r *Reader) GetPayloadFiles(pattern, payloadRange, output string) error {
|
|
r.initFileList()
|
|
pre := regexp.MustCompile(`^payload.\d+$`)
|
|
if payloadRange != "" {
|
|
pre = regexp.MustCompile(payloadRange)
|
|
}
|
|
for _, file := range r.Files() {
|
|
if file.isDir {
|
|
continue
|
|
}
|
|
if pre.MatchString(file.Base()) {
|
|
f, err := r.Open(file.Name(), false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmpdir, err := os.MkdirTemp("", "ota_payload_extract")
|
|
if err != nil {
|
|
_ = f.Close()
|
|
return err
|
|
}
|
|
if err := aaExtractPattern(f, pattern, tmpdir); err != nil {
|
|
_ = f.Close()
|
|
_ = os.RemoveAll(tmpdir)
|
|
return err
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
_ = os.RemoveAll(tmpdir)
|
|
return err
|
|
}
|
|
if err := filepath.Walk(tmpdir, func(path string, f os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !f.IsDir() {
|
|
rel, err := filepath.Rel(tmpdir, path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to compute relative path for %s: %v", path, err)
|
|
}
|
|
fname, err := utils.SanitizeArchivePath(output, rel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(fname), 0o750); err != nil {
|
|
return fmt.Errorf("failed to create dir %s: %v", filepath.Dir(fname), err)
|
|
}
|
|
utils.Indent(log.Info, 2)(fmt.Sprintf("Extracting from '%s' -> %s\t%s", file.Base(), humanize.Bytes(uint64(f.Size())), fname))
|
|
if err := os.Rename(path, fname); err != nil {
|
|
return fmt.Errorf("failed to mv file %s to %s: %v", rel, fname, err)
|
|
}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
_ = os.RemoveAll(tmpdir)
|
|
return fmt.Errorf("failed to read files in tmp folder: %v", err)
|
|
}
|
|
if err := os.RemoveAll(tmpdir); err != nil {
|
|
return fmt.Errorf("failed to remove tmp OTA payload dir %s: %v", tmpdir, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Reader) PayloadFiles(pattern string, json bool) error {
|
|
r.initFileList()
|
|
pre := regexp.MustCompile(`^payload.\d+$`)
|
|
for _, file := range r.Files() {
|
|
if file.isDir {
|
|
continue
|
|
}
|
|
if pre.MatchString(file.Base()) {
|
|
f, err := r.Open(file.Name(), false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out, err := aaList(f, pattern, json)
|
|
if closeErr := f.Close(); closeErr != nil && err == nil {
|
|
err = closeErr
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(out) > 0 && out != "[]" {
|
|
fmt.Println(out)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func aaList(in io.Reader, pattern string, json bool) (string, error) {
|
|
aaPath, err := execabs.LookPath("aa")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
args := []string{"list", "-exclude-field", "all", "-include-field", "attr"}
|
|
|
|
if len(pattern) > 0 {
|
|
args = append(args, []string{"-include-regex", pattern}...)
|
|
}
|
|
if json {
|
|
args = append(args, []string{"-list-format", "json"}...)
|
|
}
|
|
if len(pattern) == 0 && !json {
|
|
args = append(args, "-v")
|
|
}
|
|
|
|
cmd := exec.Command(aaPath, args...)
|
|
cmd.Stdin = in
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
|
|
return strings.TrimSpace(string(out)), nil
|
|
}
|
|
|
|
func aaExtractPattern(in io.Reader, pattern, output string) error {
|
|
aaPath, err := execabs.LookPath("aa")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd := exec.Command(aaPath, "extract", "-d", output, "-include-regex", pattern)
|
|
cmd.Stdin = in
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
if _, ok := err.(*exec.ExitError); !ok {
|
|
return fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Reader) ExtractCryptex(cryptex, output string) (string, error) {
|
|
var re *regexp.Regexp
|
|
switch cryptex {
|
|
case "system":
|
|
re = regexp.MustCompile(`cryptex-system-(arm64e?|x86_64h?)$`)
|
|
case "system-arm64e":
|
|
re = regexp.MustCompile(`cryptex-system-arm64e$`)
|
|
case "system-x86_64h":
|
|
re = regexp.MustCompile(`cryptex-system-x86_64h$`)
|
|
case "app":
|
|
re = regexp.MustCompile(`cryptex-app$`)
|
|
default:
|
|
return "", fmt.Errorf("unknown cryptex type '%s'", cryptex)
|
|
}
|
|
|
|
tmpdir, err := os.MkdirTemp("", "ota_extract_cryptexes")
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpdir)
|
|
|
|
for _, file := range r.Files() {
|
|
if re.MatchString(file.Base()) {
|
|
cryptexFile, err := r.Open(file.Name(), false)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to open cryptex file: %v", err)
|
|
}
|
|
defer cryptexFile.Close()
|
|
// create a temp file to hold the OTA cryptex
|
|
cf, err := os.Create(filepath.Join(tmpdir, file.Base()))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create file: %v", err)
|
|
}
|
|
// create a temp file to hold the PATCHED OTA cryptex DMG
|
|
dcf, err := os.Create(filepath.Join(output, file.Base()+".dmg"))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create file: %v", err)
|
|
}
|
|
dcf.Close()
|
|
if _, err := io.Copy(cf, cryptexFile); err != nil {
|
|
return "", fmt.Errorf("failed to write file: %v", err)
|
|
}
|
|
cf.Close()
|
|
// patch the cryptex
|
|
if err := ridiff.RawImagePatch("", cf.Name(), dcf.Name(), 0); err != nil {
|
|
return "", fmt.Errorf("failed to patch %s: %v", filepath.Base(file.Name()), err)
|
|
}
|
|
return dcf.Name(), nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("%w: '%s'", ErrCryptexNotFound, cryptex)
|
|
}
|
|
|
|
func (r *Reader) ExtractFromCryptexes(pattern, output string) ([]string, error) {
|
|
var out []string
|
|
|
|
match, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to compile extract regex pattern '%s': %v", pattern, err)
|
|
}
|
|
|
|
tmpdir, err := os.MkdirTemp("", "ota_extract_cryptexes")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpdir)
|
|
|
|
for _, cryptex := range []string{"cryptex-system-(arm64e?|x86_64h?)$"} {
|
|
re := regexp.MustCompile(cryptex)
|
|
for _, file := range r.Files() {
|
|
if re.MatchString(file.Base()) {
|
|
cryptexFile, err := r.Open(file.Name(), false)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open cryptex file: %v", err)
|
|
}
|
|
defer cryptexFile.Close()
|
|
// create a temp file to hold the OTA cryptex
|
|
cf, err := os.Create(filepath.Join(tmpdir, file.Base()))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create file: %v", err)
|
|
}
|
|
// create a temp file to hold the PATCHED OTA cryptex DMG
|
|
dcf, err := os.Create(filepath.Join(tmpdir, file.Base()+".dmg"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create file: %v", err)
|
|
}
|
|
if _, err := io.Copy(cf, cryptexFile); err != nil {
|
|
return nil, fmt.Errorf("failed to write file: %v", err)
|
|
}
|
|
cf.Close()
|
|
// patch the cryptex
|
|
if err := ridiff.RawImagePatch("", cf.Name(), dcf.Name(), 0); err != nil {
|
|
return nil, fmt.Errorf("failed to patch %s: %v", filepath.Base(file.Name()), err)
|
|
}
|
|
dcf.Close()
|
|
// mount the patched cryptex
|
|
utils.Indent(log.Info, 4)(fmt.Sprintf("Mounting DMG %s", dcf.Name()))
|
|
mountPoint, alreadyMounted, err := utils.MountDMG(dcf.Name(), "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to IPSW FS dmg: %v", err)
|
|
}
|
|
if alreadyMounted {
|
|
utils.Indent(log.Debug, 5)(fmt.Sprintf("%s already mounted", dcf.Name()))
|
|
} else {
|
|
defer func() {
|
|
utils.Indent(log.Debug, 4)(fmt.Sprintf("Unmounting %s", dcf.Name()))
|
|
if err := utils.Retry(3, 2*time.Second, func() error {
|
|
return utils.Unmount(mountPoint, true)
|
|
}); err != nil {
|
|
log.Errorf("failed to unmount DMG %s at %s: %v", dcf.Name(), mountPoint, err)
|
|
}
|
|
}()
|
|
}
|
|
// extract files from the mounted cryptex
|
|
if err := filepath.Walk(mountPoint, func(path string, info fs.FileInfo, err error) error {
|
|
if err != nil {
|
|
return fmt.Errorf("failed to walk %s: %v", path, err)
|
|
}
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
if match.MatchString(path) {
|
|
fname := filepath.Join(output, strings.TrimPrefix(path, mountPoint))
|
|
if err := utils.MkdirAndCopy(path, fname); err != nil {
|
|
return fmt.Errorf("failed to copy %s to %s: %v", path, fname, err)
|
|
}
|
|
out = append(out, fname)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
if errors.Is(err, filepath.SkipDir) {
|
|
break
|
|
}
|
|
return nil, fmt.Errorf("failed to read files in cryptex folder: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(out) == 0 {
|
|
return nil, fmt.Errorf("no files found matching pattern '%s'", pattern)
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// Open opens the named file in the ZIP archive,
|
|
// using the semantics of fs.FS.Open:
|
|
// paths are always slash separated, with no
|
|
// leading / or ../ elements.
|
|
func (r *Reader) Open(name string, decomp bool) (fs.File, error) {
|
|
r.initFileList()
|
|
|
|
if !fs.ValidPath(name) {
|
|
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
|
|
}
|
|
e := r.openLookup(name)
|
|
if e == nil {
|
|
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
|
}
|
|
rc, err := e.Open(decomp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return rc.(fs.File), nil
|
|
}
|
|
|
|
func (r *Reader) OpenInPayload(name string) (fs.File, error) {
|
|
if err := r.initPayloadMap(); err != nil {
|
|
return nil, err
|
|
}
|
|
if !fs.ValidPath(name) {
|
|
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
|
|
}
|
|
payload := r.payloadLookuo(name)
|
|
if payload == "" {
|
|
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
|
}
|
|
rc, err := r.Open(payload, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return rc, nil
|
|
}
|
|
|
|
func split(name string) (dir, elem string, isDir bool) {
|
|
if len(name) > 0 && name[len(name)-1] == '/' {
|
|
isDir = true
|
|
name = name[:len(name)-1]
|
|
}
|
|
i := len(name) - 1
|
|
for i >= 0 && name[i] != '/' {
|
|
i--
|
|
}
|
|
if i < 0 {
|
|
return ".", name, isDir
|
|
}
|
|
return name[:i], name[i+1:], isDir
|
|
}
|
|
|
|
var dotFile = &File{name: "./", isDir: true}
|
|
|
|
func (r *Reader) openLookup(name string) *File {
|
|
if name == "." {
|
|
return dotFile
|
|
}
|
|
dir, elem, _ := split(name)
|
|
files := r.fileList
|
|
i := sort.Search(len(files), func(i int) bool {
|
|
idir, ielem, _ := split(files[i].name)
|
|
return idir > dir || idir == dir && ielem >= elem
|
|
})
|
|
if i < len(files) {
|
|
fname := files[i].name
|
|
if fname == name || len(fname) == len(name)+1 && fname[len(name)] == '/' && fname[:len(name)] == name {
|
|
return files[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Reader) payloadLookuo(name string) string {
|
|
dir, elem, _ := split(name)
|
|
startFiles := maps.Values(r.payloadMap)
|
|
sort.Strings(startFiles)
|
|
i := sort.Search(len(startFiles), func(i int) bool {
|
|
idir, ielem, _ := split(startFiles[i])
|
|
return idir > dir || idir == dir && ielem >= elem
|
|
})
|
|
if i < len(startFiles) {
|
|
for k, v := range r.payloadMap {
|
|
if k == startFiles[i] {
|
|
return v
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (f *File) Base() string { _, elem, _ := split(f.name); return elem }
|
|
func (f *File) Name() string { return f.name }
|
|
func (f *File) Size() int64 {
|
|
if f.zfile != nil {
|
|
return int64(f.zfile.UncompressedSize64)
|
|
} else {
|
|
return int64(f.entry.Size)
|
|
}
|
|
}
|
|
func (f *File) Mode() fs.FileMode {
|
|
if f.zfile != nil {
|
|
return f.zfile.Mode()
|
|
} else {
|
|
return f.entry.Mod
|
|
}
|
|
}
|
|
func (f *File) ModTime() time.Time {
|
|
if f.zfile != nil {
|
|
return f.zfile.Modified
|
|
} else {
|
|
return f.entry.Mtm
|
|
}
|
|
}
|
|
func (f *File) Type() fs.FileMode { return fs.ModeDir }
|
|
func (f *File) IsDir() bool { return f.isDir }
|
|
func (f *File) Sys() any { return nil }
|
|
func (f *File) Stat() (fs.FileInfo, error) { return f, nil }
|
|
|
|
type otaReader struct {
|
|
rc io.ReadCloser
|
|
f *File
|
|
}
|
|
|
|
func (r *otaReader) Stat() (fs.FileInfo, error) {
|
|
return r.f, nil
|
|
}
|
|
|
|
func (r *otaReader) Read(b []byte) (n int, err error) {
|
|
return r.rc.Read(b)
|
|
}
|
|
|
|
func (r *otaReader) Close() error { return r.rc.Close() }
|
|
|
|
func (f *File) Open(decomp bool) (io.ReadCloser, error) {
|
|
var mdata [4]byte
|
|
var rc io.ReadCloser
|
|
if f.zfile != nil {
|
|
zf, err := f.zfile.Open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := zf.Read(mdata[:]); err != nil {
|
|
zf.Close()
|
|
if err == io.EOF {
|
|
return f.zfile.Open()
|
|
}
|
|
return nil, err
|
|
}
|
|
zf.Close()
|
|
switch magic.Magic(binary.BigEndian.Uint32(mdata[:])) {
|
|
case magic.MagicPBZX:
|
|
if decomp {
|
|
var pbuf bytes.Buffer
|
|
zf, err := f.zfile.Open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer zf.Close()
|
|
if err := pbzx.Extract(context.Background(), zf, &pbuf, runtime.NumCPU()); err != nil {
|
|
return nil, err
|
|
}
|
|
rc = &otaReader{
|
|
rc: io.NopCloser(bytes.NewReader(pbuf.Bytes())),
|
|
f: f,
|
|
}
|
|
return rc, nil
|
|
}
|
|
fallthrough
|
|
default:
|
|
return f.zfile.Open()
|
|
}
|
|
}
|
|
|
|
if _, err := f.entry.Read(mdata[:]); err != nil {
|
|
if err == io.EOF {
|
|
rc = &otaReader{
|
|
rc: io.NopCloser(bytes.NewReader([]byte{})),
|
|
f: f,
|
|
}
|
|
return rc, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
switch magic.Magic(binary.BigEndian.Uint32(mdata[:])) {
|
|
case magic.MagicPBZX:
|
|
if decomp {
|
|
data := make([]byte, f.entry.Size)
|
|
if _, err := f.entry.Read(data); err != nil {
|
|
return nil, err
|
|
}
|
|
var pbuf bytes.Buffer
|
|
if err := pbzx.Extract(context.Background(), bytes.NewReader(data), &pbuf, runtime.NumCPU()); err != nil {
|
|
return nil, err
|
|
}
|
|
rc = &otaReader{
|
|
rc: io.NopCloser(bytes.NewReader(pbuf.Bytes())),
|
|
f: f,
|
|
}
|
|
return rc, nil
|
|
}
|
|
fallthrough
|
|
default:
|
|
edata := make([]byte, f.entry.Size)
|
|
if _, err := f.entry.Read(edata); err != nil {
|
|
return nil, err
|
|
}
|
|
rc = &otaReader{
|
|
rc: io.NopCloser(bytes.NewReader(edata)),
|
|
f: f,
|
|
}
|
|
return rc, nil
|
|
}
|
|
}
|