mirror of
https://github.com/blacktop/ipsw.git
synced 2026-06-07 12:27:36 +00:00
828 lines
26 KiB
Go
828 lines
26 KiB
Go
package img4
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"encoding/asn1"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"hash/adler32"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/apex/log"
|
|
"github.com/blacktop/ipsw/internal/magic"
|
|
"github.com/blacktop/ipsw/internal/utils"
|
|
"github.com/blacktop/ipsw/pkg/comp"
|
|
"github.com/blacktop/lzss"
|
|
"github.com/dustin/go-humanize"
|
|
)
|
|
|
|
// IM4P Types - these are the four-character codes used to identify different payload types in IMG4 containers
|
|
const (
|
|
// ACI Types
|
|
IM4P_ACIBT = "acib" // ACI Bluetooth
|
|
IM4P_ACIWIFI = "aciw" // ACI WiFi
|
|
|
|
// Firmware Types
|
|
IM4P_ALAMO = "almo" // Alamo
|
|
IM4P_ANE_FIRMWARE = "anef" // ANE Firmware
|
|
IM4P_AOP = "aopf" // Always on processor firmware
|
|
IM4P_ANS_FIRMWARE = "ansf" // Apple NVME Storage controller firmware
|
|
IM4P_AUDIO_CODEC_FW = "acfw" // Audio Codec Firmware
|
|
IM4P_AVE_FIRMWARE = "avef" // AVE Firmware
|
|
IM4P_GPU_FIRMWARE = "gfxf" // GPU Firmware
|
|
IM4P_MAGGIE_FIRMWARE = "magg" // Maggie Firmware
|
|
IM4P_MULTITOUCH_FW = "mtfw" // Multitouch Firmware
|
|
IM4P_PMP = "pmpf" // PMP
|
|
IM4P_RTP = "rtpf" // RTP
|
|
IM4P_SCE = "scef" // SCE
|
|
IM4P_SMARTIO_FW = "siof" // SmartIOFirmware
|
|
IM4P_WCH_FW_UPDATER = "wchf" // WCHFirmwareUpdater
|
|
IM4P_WIRELESS_CHARGING = "wchf" // Wireless Charging
|
|
|
|
// Boot Components
|
|
IM4P_IBEC = "ibec" // iBEC
|
|
IM4P_IBOOT = "ibot" // iBoot
|
|
IM4P_IBOOT_TEST = "itst" // iBootTest
|
|
IM4P_IBSS = "ibss" // iBSS
|
|
IM4P_LLB = "illb" // LLB
|
|
IM4P_CFE_LOADER = "cfel" // Silicon Validation CFE loader
|
|
IM4P_IBOOT_DATA = "ibdt" // iBoot Data
|
|
|
|
// System Components
|
|
IM4P_KERNELCACHE = "krnl" // KernelCache
|
|
IM4P_DEVICE_TREE = "dtre" // DeviceTree
|
|
IM4P_SEP = "sepi" // SEP
|
|
IM4P_HYPERVISOR = "hypr" // Hypervisor
|
|
IM4P_DIAGS = "diag" // Diags
|
|
IM4P_INPUT_DEVICE = "ipdf" // Input Device
|
|
IM4P_ISPOOF = "ispf" // iSpoof(?)
|
|
IM4P_ENVIRONMENT_VARS = "ienv" // Environment Variables
|
|
|
|
// Display Components
|
|
IM4P_APPLE_LOGO = "logo" // Used during boot
|
|
IM4P_DCP_FW = "dcpf" // Display Coprocessor firmware, used on the iPhone 12 and later
|
|
IM4P_DALI = "dali" // Dali
|
|
IM4P_HOMER = "homr" // Homer
|
|
|
|
// Battery/Charging UI
|
|
IM4P_BATTERY_CHARGING0 = "chg0" // Charging indicator (bright), used in the battery low screen
|
|
IM4P_BATTERY_CHARGING1 = "chg1" // Charging Indicator (dim), used in the battery low screen
|
|
IM4P_BATTERY_FULL = "batF" // Only used in China to indicate battery status while device is off
|
|
IM4P_BATTERY_LOW0 = "bat0" // Empty battery, used in the battery low screen
|
|
IM4P_BATTERY_LOW1 = "bat1" // Red composed onto empty battery, used in the battery low screen
|
|
IM4P_GLYPH_CHARGING = "glyC" // Used in the battery low screen
|
|
IM4P_GLYPH_PLUGIN = "glyP" // Used in the battery low screen
|
|
IM4P_LOW_POWER_WALLET0 = "lpw0" // Used in the battery low screen with power reserve enabled
|
|
IM4P_LOW_POWER_WALLET1 = "lpw1" // Used in the battery low screen with power reserve enabled
|
|
IM4P_LOW_POWER_WALLET2 = "lpw2" // Used in the battery low screen with power reserve enabled
|
|
IM4P_NEED_SERVICE = "nsrv" // Need Service screen (removed in iOS 4.0+)
|
|
IM4P_RECOVERY_MODE = "recm" // Used when device is in Recovery Mode
|
|
|
|
// Trust Cache Types
|
|
IM4P_ENGINEERING_TRUST_CACHE = "dtrs" // EngineeringTrustCache
|
|
IM4P_LOADABLE_TRUST_CACHE = "ltrs" // LoadableTrustCache
|
|
IM4P_STATIC_TRUST_CACHE = "trst" // StaticTrustCache
|
|
IM4P_BASE_SYSTEM_TRUST_CACHE = "bstc" // Base System Trust Cache
|
|
IM4P_X86_BASE_SYSTEM_TC = "xbtc" // x86 Base System Trust Cache
|
|
|
|
// Restore Components
|
|
IM4P_RESTORE_ANS_FW = "rans" // Restore Apple NVME storage firmware
|
|
IM4P_RESTORE_RAMDISK = "rdsk" // RestoreRamDisk
|
|
IM4P_RESTORE_DEVICE_TREE = "rdtr" // RestoreDeviceTree
|
|
IM4P_RESTORE_KERNEL_CACHE = "rkrn" // RestoreKernelCache
|
|
IM4P_RESTORE_LOGO = "rlgo" // Same as appleLogo
|
|
IM4P_RESTORE_OS = "rosi" // RestoreOS
|
|
IM4P_RESTORE_DCP_FW = "dcpf" // Restore Display Coprocessor firmware, used on the iPhone 12 and later
|
|
IM4P_RESTORE_SEP = "rsep" // rsep is also present in sepboot
|
|
IM4P_RESTORE_TRUST_CACHE = "rtsc" // RestoreTrustCache
|
|
|
|
// FDR Trust Objects
|
|
IM4P_FDR_TRUST_OBJECT_AP = "fdrt" // FDR Trust Object for AP
|
|
IM4P_FDR_TRUST_OBJECT_SEP = "fdrs" // FDR Trust Object for SEP
|
|
|
|
// Audio Files
|
|
IM4P_BOOT_CHIME = "aubt" // Raw audio file
|
|
IM4P_ACCESSIBILITY_CHIME = "auac" // Raw audio file
|
|
IM4P_ATTACH_CHIME = "aupr" // Raw audio file
|
|
|
|
// System Volume Components
|
|
IM4P_AUX_KERNEL_CACHE = "auxk" // Aux Kernel Cache
|
|
IM4P_BASE_SYSTEM_ROOT_HASH = "csys" // Base System Volume Root Hash
|
|
IM4P_SYSTEM_VOLUME_ROOT_HASH = "isys" // System Volume Root Hash
|
|
IM4P_X86_SYSTEM_VOLUME_HASH = "xsys" // x86 System Volume Root hash
|
|
IM4P_SYSTEM_VOLUME_METADATA = "msys" // System Volume Canonical Metadata (Compressed gzip file)
|
|
IM4P_X86_SYSTEM_VOLUME_METADATA = "xsys" // x86 System Volume Canonical Metadata (Compressed gzip file)
|
|
|
|
// Cryptex Components
|
|
IM4P_CRYPTEX_INFO = "ginf" // Cryptex Info.plist metadata
|
|
IM4P_CRYPTEX_DISK_IMAGE = "gdmg" // Cryptex disk image
|
|
IM4P_CRYPTEX_SEAL = "gtgv" // Cryptex APFS root hash tree seal
|
|
IM4P_CRYPTEX_TRUST_CACHE = "gtcd" // Cryptex trust cache cdhash list
|
|
|
|
// Special/Testing Components
|
|
IM4P_AP_TICKET = "SCAB" // On devices that don't use IMG4
|
|
IM4P_OS_RAMDISK = "osrd" // OSRamdisk
|
|
IM4P_PE_HAMMER = "hmmr" // PE Hammer test
|
|
IM4P_PERSONALIZED_DMG = "pdmg" // PersonalizedDMG
|
|
IM4P_PERTOS = "pert" // PE RTOS environment
|
|
IM4P_PHLEET = "phlt" // Silicon Validation PHLEET test
|
|
IM4P_RBM = "rbmt" // Silicon Validation RBM test
|
|
IM4P_SYSTEM_LOCKER = "lckr" // System Locker
|
|
IM4P_TSYS_TESTER = "tsys" // TSYS Tester
|
|
IM4P_LEAP_HAPTICS = "lphp" // LeapHaptics
|
|
IM4P_MAC_EFI = "mefi" // Used only on T2 Macs
|
|
|
|
// Unknown Types
|
|
IM4P_CIOF = "ciof" // Unknown
|
|
IM4P_TMUF = "tmuf" // Unknown
|
|
IM4P_RPMP = "rpmp" // Unknown
|
|
IM4P_LPOL = "lpol" // Unknown
|
|
IM4P_RTMU = "rtmu" // Unknown
|
|
IM4P_RCIO = "rcio" // Unknown
|
|
)
|
|
|
|
var ErrNotCompressed = fmt.Errorf("payload is not compressed")
|
|
|
|
// CompressionTypes is a list of supported compression algorithms
|
|
var CompressionTypes = []string{"none", "lzss", "lzfse", "lzfse_iboot"}
|
|
|
|
type CompressionAlgorithm int
|
|
|
|
const (
|
|
CompressionAlgorithmLZSS CompressionAlgorithm = iota
|
|
CompressionAlgorithmLZFSE
|
|
CompressionAlgorithmMAX
|
|
)
|
|
|
|
func (c CompressionAlgorithm) String() string {
|
|
switch c {
|
|
case CompressionAlgorithmLZSS:
|
|
return "LZSS"
|
|
case CompressionAlgorithmLZFSE:
|
|
return "LZFSE"
|
|
default:
|
|
return fmt.Sprintf("Unknown (%d)", c)
|
|
}
|
|
}
|
|
|
|
type Compression struct {
|
|
Algorithm CompressionAlgorithm `asn1:"integer" json:"algorithm,omitempty"`
|
|
UncompressedSize int `asn1:"integer" json:"uncompressed_size,omitempty"`
|
|
}
|
|
|
|
type PAYP struct {
|
|
Raw asn1.RawContent
|
|
Tag string `asn1:"ia5"` // PAYP
|
|
Set asn1.RawValue `asn1:"set"`
|
|
}
|
|
|
|
type IM4P struct {
|
|
Raw asn1.RawContent
|
|
Tag string `asn1:"ia5"` // IM4P
|
|
Type string `asn1:"ia5"`
|
|
Version string `asn1:"ia5"`
|
|
Data []byte
|
|
Compression Compression `asn1:"optional,omitempty"`
|
|
Keybag []byte `asn1:"optional"`
|
|
Properties PAYP `asn1:"optional,tag:0,class:context,explicit"`
|
|
Hash []byte `asn1:"optional"`
|
|
}
|
|
|
|
type Payload struct {
|
|
IM4P
|
|
Encrypted bool
|
|
Keybags []Keybag
|
|
Properties map[string]any
|
|
|
|
decompressedData []byte
|
|
extraData []byte // Any extra data appended after the compressed payload
|
|
}
|
|
|
|
func OpenPayload(path string) (*Payload, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open payload %s: %v", path, err)
|
|
}
|
|
defer func() {
|
|
if err := f.Close(); err != nil {
|
|
log.Errorf("failed to close payload %s: %v", path, err)
|
|
}
|
|
}()
|
|
data, err := io.ReadAll(f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read payload %s: %v", path, err)
|
|
}
|
|
return ParsePayload(data)
|
|
}
|
|
|
|
func ParsePayload(data []byte) (*Payload, error) {
|
|
|
|
if len(data) == 0 {
|
|
return nil, fmt.Errorf("empty payload data")
|
|
}
|
|
|
|
var p Payload
|
|
rest, err := asn1.Unmarshal(data, &p.IM4P)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to ASN.1 parse IM4P: %v", err)
|
|
}
|
|
if len(rest) > 0 {
|
|
log.Warnf("unexpected trailing data after IM4P structure: %d bytes", len(rest))
|
|
}
|
|
|
|
if p.Tag != "IM4P" {
|
|
return nil, fmt.Errorf("invalid payload tag: expected 'IM4P', got '%s'", p.Tag)
|
|
}
|
|
|
|
if p.Keybag != nil {
|
|
if _, err := asn1.Unmarshal(p.Keybag, &p.Keybags); err != nil {
|
|
return nil, fmt.Errorf("failed to ASN.1 parse IM4P KBAG: %v", err)
|
|
}
|
|
if len(p.Keybags) > 0 {
|
|
p.Encrypted = true
|
|
}
|
|
}
|
|
|
|
if len(p.IM4P.Properties.Raw) > 0 {
|
|
if p.IM4P.Properties.Tag != "PAYP" {
|
|
return nil, fmt.Errorf("invalid payload properties tag: expected 'PAYP', got '%s'", p.IM4P.Properties.Tag)
|
|
}
|
|
p.Properties, err = ParsePropertyMap(p.IM4P.Properties.Set.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse payload properties: %v", err)
|
|
}
|
|
}
|
|
|
|
return &p, nil
|
|
}
|
|
|
|
func (p *Payload) String() string {
|
|
var sb strings.Builder
|
|
sb.WriteString(fmt.Sprintf("%s:\n", colorTitle("IM4P (Payload)")))
|
|
sb.WriteString(fmt.Sprintf(" %s: %s\n", colorField("Tag"), p.Tag))
|
|
sb.WriteString(fmt.Sprintf(" %s: %s\n", colorField("Type"), p.Type))
|
|
if len(p.Version) > 0 {
|
|
sb.WriteString(fmt.Sprintf(" %s: %s\n", colorField("Version"), p.Version))
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" %s: %s (%d bytes)\n", colorField("Data"), humanize.Bytes(uint64(len(p.Data))), len(p.Data)))
|
|
if p.Compression.UncompressedSize > 0 {
|
|
sb.WriteString(fmt.Sprintf(" %s: %s\n", colorField("Compression"), p.Compression.Algorithm.String()))
|
|
if p.Compression.UncompressedSize > 0 {
|
|
sb.WriteString(fmt.Sprintf(" %s: %s (%d bytes)\n", colorField("Uncompressed"), humanize.Bytes(uint64(p.Compression.UncompressedSize)), p.Compression.UncompressedSize))
|
|
}
|
|
}
|
|
if p.HasExtraData() {
|
|
extraData := p.GetExtraData()
|
|
if len(extraData) > 0 {
|
|
sb.WriteString(fmt.Sprintf(" %s: %s (%d bytes)\n", colorField("ExtraData"), humanize.Bytes(uint64(len(extraData))), len(extraData)))
|
|
}
|
|
}
|
|
if len(p.Keybags) > 0 {
|
|
sb.WriteString(fmt.Sprintf(" %s: %t\n", colorField("Encrypted"), p.Encrypted))
|
|
sb.WriteString(fmt.Sprintf(" %s:\n", colorField("Keybags")))
|
|
for i, kb := range p.Keybags {
|
|
sb.WriteString(fmt.Sprintf(" [%d] %s: %s\n", i, colorField("Type"), kb.Type.String()))
|
|
sb.WriteString(fmt.Sprintf(" %s: %x\n", colorField("IV"), kb.IV))
|
|
sb.WriteString(fmt.Sprintf(" %s: %x\n", colorField("Key"), kb.Key))
|
|
}
|
|
}
|
|
if p.Properties != nil {
|
|
sb.WriteString(fmt.Sprintf(" %s:\n", colorField("Properties")))
|
|
for k, v := range p.Properties {
|
|
sb.WriteString(fmt.Sprintf(" %s: %v\n", colorSubField(k), FormatPropertyValue(v)))
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
func (p *Payload) MarshalJSON() ([]byte, error) {
|
|
data := map[string]any{
|
|
"tag": p.Tag,
|
|
"type": p.Type,
|
|
"version": p.Version,
|
|
"data_size": len(p.Data),
|
|
"encrypted": p.Encrypted,
|
|
"keybags": p.Keybags,
|
|
"properties": p.Properties,
|
|
"compression": p.Compression,
|
|
"hash": p.Hash,
|
|
}
|
|
if p.HasExtraData() {
|
|
data["extra_data_size"] = len(p.GetExtraData())
|
|
}
|
|
return json.Marshal(data)
|
|
}
|
|
|
|
func (p *Payload) Marshal() ([]byte, error) {
|
|
return asn1.Marshal(p.IM4P)
|
|
}
|
|
|
|
func (p *Payload) Decompress() ([]byte, error) {
|
|
if p.decompressedData != nil {
|
|
return p.decompressedData, nil
|
|
}
|
|
// confirm that the payload is compressed and detect compression type
|
|
isCompressed := false
|
|
hasCompressField := false
|
|
|
|
var detectedAlgorithm CompressionAlgorithm
|
|
if p.Compression.UncompressedSize > 0 {
|
|
isCompressed = true
|
|
hasCompressField = true
|
|
detectedAlgorithm = p.Compression.Algorithm
|
|
} else if isLzss, err := magic.IsLZSS(p.Data); err != nil {
|
|
return nil, fmt.Errorf("failed to check if data is LZSS: %v", err)
|
|
} else if isLzss {
|
|
isCompressed = true
|
|
detectedAlgorithm = CompressionAlgorithmLZSS
|
|
} else if isLzfse, err := magic.IsLZFSE(p.Data); err != nil {
|
|
return nil, fmt.Errorf("failed to check if data is LZFSE: %v", err)
|
|
} else if isLzfse {
|
|
isCompressed = true
|
|
detectedAlgorithm = CompressionAlgorithmLZFSE
|
|
}
|
|
if !isCompressed {
|
|
return nil, ErrNotCompressed
|
|
}
|
|
|
|
switch detectedAlgorithm {
|
|
case CompressionAlgorithmLZSS:
|
|
if len(p.Data) < binary.Size(lzss.Header{}) {
|
|
return nil, fmt.Errorf("data too short to contain valid LZSS header")
|
|
}
|
|
var hdr lzss.Header
|
|
if err := binary.Read(bytes.NewReader(p.Data[:binary.Size(hdr)]), binary.BigEndian, &hdr); err != nil {
|
|
return nil, fmt.Errorf("failed to read LZSS header: %v", err)
|
|
}
|
|
if hdr.Signature != lzss.Signature || hdr.CompressionType != lzss.CompressionType {
|
|
return nil, fmt.Errorf("invalid LZSS header magic: %x", string(p.Data[:8]))
|
|
}
|
|
if hdr.CompressedSize == 0 {
|
|
return nil, fmt.Errorf("LZSS header indicates zero compressed size")
|
|
}
|
|
if len(p.Data) < int(hdr.CompressedSize)+binary.Size(hdr) {
|
|
return nil, fmt.Errorf("data too short to contain valid LZSS compressed payload")
|
|
}
|
|
// Only decompress the compressed payload part, not any extra data
|
|
compressedData := p.Data[binary.Size(hdr) : binary.Size(hdr)+int(hdr.CompressedSize)]
|
|
decompressed := lzss.Decompress(compressedData)
|
|
if len(decompressed) == 0 {
|
|
return nil, fmt.Errorf("failed to decompress LZSS payload: got empty result")
|
|
}
|
|
// Trim decompressed data to the expected uncompressed size
|
|
if len(decompressed) > int(hdr.UncompressedSize) {
|
|
log.Warnf("trimming decompressed LZSS data from %d to expected size %d", len(decompressed), hdr.UncompressedSize)
|
|
decompressed = decompressed[:hdr.UncompressedSize]
|
|
} else if len(decompressed) < int(hdr.UncompressedSize) {
|
|
return nil, fmt.Errorf("decompressed LZSS size (%d) is less than expected size (%d)", len(decompressed), hdr.UncompressedSize)
|
|
}
|
|
p.decompressedData = decompressed
|
|
if len(p.Data) > int(hdr.CompressedSize)+binary.Size(hdr) {
|
|
// Extract any extra data after the LZSS payload
|
|
p.extraData = p.Data[int(hdr.CompressedSize)+binary.Size(hdr):]
|
|
if len(p.extraData) > 0 {
|
|
log.Debugf("extracted %d bytes of extra data after LZSS payload", len(p.extraData))
|
|
}
|
|
}
|
|
if !hasCompressField {
|
|
p.Compression.Algorithm = detectedAlgorithm
|
|
p.Compression.UncompressedSize = len(decompressed)
|
|
}
|
|
return decompressed, nil
|
|
case CompressionAlgorithmLZFSE:
|
|
if len(p.Data) < 4 {
|
|
return nil, fmt.Errorf("data too short to contain valid LZFSE header")
|
|
}
|
|
if isLzfse, err := magic.IsLZFSE(p.Data); err != nil {
|
|
return nil, fmt.Errorf("failed to check if data is LZFSE: %v", err)
|
|
} else if !isLzfse {
|
|
return nil, fmt.Errorf("data is not LZFSE compressed")
|
|
}
|
|
// Use the comp package for decompression
|
|
var algo comp.Algorithm
|
|
if p.Type == IM4P_IBOOT ||
|
|
p.Type == IM4P_IBOOT_DATA ||
|
|
p.Type == IM4P_IBOOT_TEST ||
|
|
p.Type == IM4P_IBEC ||
|
|
p.Type == IM4P_IBSS ||
|
|
p.Type == IM4P_LLB ||
|
|
p.Type == IM4P_CFE_LOADER {
|
|
algo = comp.LZFSE_IBOOT
|
|
} else {
|
|
algo = comp.LZFSE
|
|
}
|
|
decompressed, err := comp.Decompress(p.Data, algo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decompress %s payload: %v", detectedAlgorithm.String(), err)
|
|
}
|
|
if len(decompressed) == 0 {
|
|
return nil, fmt.Errorf("failed to decompress %s payload", detectedAlgorithm.String())
|
|
}
|
|
p.decompressedData = decompressed
|
|
// Extract any extra data after the LZFSE payload
|
|
p.extraData = p.extractExtraDataFromLzfse(p.Data)
|
|
if len(p.extraData) > 0 {
|
|
log.Debugf("extracted %d bytes of extra data after LZFSE payload", len(p.extraData))
|
|
}
|
|
if !hasCompressField {
|
|
p.Compression.Algorithm = detectedAlgorithm
|
|
p.Compression.UncompressedSize = len(decompressed)
|
|
}
|
|
return decompressed, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("unsupported compression algorithm: %v", detectedAlgorithm)
|
|
}
|
|
|
|
func (i *Payload) GetData() ([]byte, error) {
|
|
data, err := i.Decompress()
|
|
if err != nil {
|
|
if err == ErrNotCompressed {
|
|
return i.Data, nil // Return original data if not compressed
|
|
}
|
|
return nil, fmt.Errorf("failed to get decompressed data: %v", err)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// HasExtraData returns true if extra data was detected in the IM4P
|
|
func (i *Payload) HasExtraData() bool {
|
|
return len(i.GetExtraData()) > 0
|
|
}
|
|
|
|
// GetExtraData returns any extra data appended after the compressed payload
|
|
func (i *Payload) GetExtraData() []byte {
|
|
if i.decompressedData == nil {
|
|
if _, err := i.Decompress(); err != nil && err != ErrNotCompressed {
|
|
log.Errorf("failed to decompress payload to get extra data: %v", err)
|
|
return nil
|
|
}
|
|
}
|
|
return i.extraData
|
|
}
|
|
|
|
// extractExtraDataFromLzfse extracts extra data from LZFSE compressed format
|
|
// LZFSE uses block-based format, so we need to parse all blocks to find total size
|
|
func (i *Payload) extractExtraDataFromLzfse(data []byte) []byte {
|
|
// Parse LZFSE blocks to determine total compressed size
|
|
totalCompressedSize, err := i.calculateLzfseTotalSize(data)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
// Check if there's extra data after the LZFSE stream
|
|
if totalCompressedSize >= len(data) {
|
|
// No extra data
|
|
return nil
|
|
}
|
|
|
|
// Return the extra data portion
|
|
extraData := make([]byte, len(data)-totalCompressedSize)
|
|
copy(extraData, data[totalCompressedSize:])
|
|
return extraData
|
|
}
|
|
|
|
// calculateLzfseTotalSize parses LZFSE blocks to determine total compressed stream size
|
|
func (i *Payload) calculateLzfseTotalSize(data []byte) (int, error) {
|
|
if len(data) < 4 {
|
|
return 0, fmt.Errorf("data too short to contain LZFSE block magic")
|
|
}
|
|
|
|
// Verify that data starts with valid LZFSE block magic
|
|
firstMagic := binary.LittleEndian.Uint32(data[0:4])
|
|
switch firstMagic {
|
|
case 0x2d787662, // bvx- (uncompressed block)
|
|
0x31787662, // bvx1 (lzfse compressed, uncompressed tables)
|
|
0x32787662, // bvx2 (lzfse compressed, compressed tables)
|
|
0x6e787662: // bvxn (lzvn compressed)
|
|
// Valid starting block magic
|
|
default:
|
|
return 0, fmt.Errorf("data does not start with valid LZFSE block magic: %#08x", firstMagic)
|
|
}
|
|
|
|
// Search for the end-of-stream marker (bvx$)
|
|
endMarker := []byte{'b', 'v', 'x', '$'}
|
|
endOffset := bytes.Index(data, endMarker)
|
|
if endOffset != -1 {
|
|
return endOffset + 4, nil
|
|
}
|
|
|
|
// If no end-of-stream marker is found, it's an invalid LZFSE stream
|
|
return 0, fmt.Errorf("LZFSE end-of-stream marker (bvx$) not found")
|
|
}
|
|
|
|
/* PAYLOAD KEYBAGS */
|
|
|
|
type keybagType int
|
|
|
|
const (
|
|
PRODUCTION keybagType = 1
|
|
DEVELOPMENT keybagType = 2
|
|
DECRYPTED keybagType = 3
|
|
)
|
|
|
|
func (t keybagType) String() string {
|
|
switch t {
|
|
case PRODUCTION:
|
|
return "PRODUCTION"
|
|
case DEVELOPMENT:
|
|
return "DEVELOPMENT"
|
|
case DECRYPTED:
|
|
return "DECRYPTED"
|
|
default:
|
|
return fmt.Sprintf("UNKNOWN(%d)", t)
|
|
}
|
|
}
|
|
|
|
func (t keybagType) Short() string {
|
|
switch t {
|
|
case PRODUCTION:
|
|
return "prod"
|
|
case DEVELOPMENT:
|
|
return "dev"
|
|
case DECRYPTED:
|
|
return "dec"
|
|
default:
|
|
return fmt.Sprintf("unknown(%d)", t)
|
|
}
|
|
}
|
|
|
|
type Keybag struct {
|
|
Type keybagType
|
|
IV []byte
|
|
Key []byte
|
|
}
|
|
|
|
func (k Keybag) String() string {
|
|
return fmt.Sprintf(
|
|
"-\n"+
|
|
" type: %s\n"+
|
|
" iv: %x\n"+
|
|
" key: %x",
|
|
k.Type.String(),
|
|
k.IV,
|
|
k.Key)
|
|
}
|
|
|
|
func (k Keybag) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(&struct {
|
|
Type string `json:"type,omitempty"`
|
|
IV string `json:"iv,omitempty"`
|
|
Key string `json:"key,omitempty"`
|
|
}{
|
|
Type: k.Type.Short(),
|
|
IV: hex.EncodeToString(k.IV),
|
|
Key: hex.EncodeToString(k.Key),
|
|
})
|
|
}
|
|
|
|
type Im4pKeybag struct {
|
|
Name string `json:"name,omitempty"`
|
|
Keybags []Keybag `json:"kbags,omitempty"`
|
|
}
|
|
|
|
type KeybagMetaData struct {
|
|
Type string `json:"type,omitempty"`
|
|
Version string `json:"product_version,omitempty"`
|
|
Build string `json:"product_build_version,omitempty"`
|
|
Devices []string `json:"supported_product_types,omitempty"`
|
|
}
|
|
|
|
type KeyBags struct {
|
|
KeybagMetaData
|
|
Files []Im4pKeybag `json:"files,omitempty"`
|
|
}
|
|
|
|
func GetKeybagsFromIPSW(files []*zip.File, meta KeybagMetaData, pattern string) (*KeyBags, error) {
|
|
kbags := &KeyBags{KeybagMetaData: meta}
|
|
|
|
rePattern := `.*im4p$`
|
|
if len(pattern) > 0 {
|
|
if _, err := regexp.Compile(pattern); err != nil {
|
|
return nil, fmt.Errorf("failed to compile --pattern regexp: %v", err)
|
|
}
|
|
rePattern = pattern
|
|
}
|
|
|
|
for _, f := range files {
|
|
if regexp.MustCompile(rePattern).MatchString(f.Name) {
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error opening zipped file %s: %v", f.Name, err)
|
|
}
|
|
data, err := io.ReadAll(rc)
|
|
if err != nil {
|
|
log.Errorf("failed to read zipped file %s: %v", f.Name, err)
|
|
if err := rc.Close(); err != nil {
|
|
log.Errorf("failed to close zipped file %s: %v", f.Name, err)
|
|
}
|
|
continue
|
|
}
|
|
im4p, err := ParsePayload(data)
|
|
if err != nil {
|
|
log.Errorf("failed to parse im4p %s: %v", f.Name, err)
|
|
}
|
|
if im4p.Keybags == nil { // kbags are optional
|
|
continue
|
|
}
|
|
kbags.Files = append(kbags.Files, Im4pKeybag{
|
|
Name: filepath.Base(f.Name),
|
|
Keybags: im4p.Keybags,
|
|
})
|
|
if err := rc.Close(); err != nil {
|
|
log.Errorf("failed to close zipped file %s: %v", f.Name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return kbags, nil
|
|
}
|
|
|
|
/* CREATE PAYLOAD */
|
|
|
|
type CreatePayloadConfig struct {
|
|
Type string
|
|
Version string
|
|
Data []byte
|
|
ExtraData []byte // Optional extra data to append after the main data
|
|
Compression string
|
|
Keybags []Keybag
|
|
}
|
|
|
|
// CreatePayload creates a new IM4P structure
|
|
func CreatePayload(conf *CreatePayloadConfig) (*Payload, error) {
|
|
var pdata []byte
|
|
|
|
var compAlgo CompressionAlgorithm
|
|
switch strings.ToLower(conf.Compression) {
|
|
case "lzss":
|
|
compAlgo = CompressionAlgorithmLZSS
|
|
case "lzfse", "lzfse_iboot":
|
|
compAlgo = CompressionAlgorithmLZFSE
|
|
case "none", "":
|
|
compAlgo = CompressionAlgorithmMAX // No compression
|
|
}
|
|
|
|
switch compAlgo {
|
|
case CompressionAlgorithmLZSS:
|
|
compressedData := lzss.Compress(conf.Data)
|
|
if len(compressedData) == 0 {
|
|
return nil, fmt.Errorf("failed to LZSS compress data")
|
|
}
|
|
utils.Indent(log.Debug, 2)(
|
|
fmt.Sprintf("LZSS compression: %d → %d bytes (%.1f%% reduction)",
|
|
len(conf.Data), len(compressedData),
|
|
float64(len(conf.Data)-len(compressedData))/float64(len(conf.Data))*100),
|
|
)
|
|
hdr := lzss.Header{
|
|
Signature: lzss.Signature,
|
|
CompressionType: lzss.CompressionType,
|
|
CheckSum: adler32.Checksum(conf.Data), // Calculate Adler32 of uncompressed data
|
|
UncompressedSize: uint32(len(conf.Data)),
|
|
CompressedSize: uint32(len(compressedData)),
|
|
Version: 1, // LZSS version
|
|
}
|
|
buf := new(bytes.Buffer)
|
|
if err := binary.Write(buf, binary.BigEndian, hdr); err != nil {
|
|
return nil, fmt.Errorf("failed to write LZSS header: %v", err)
|
|
}
|
|
if _, err := buf.Write(compressedData); err != nil {
|
|
return nil, fmt.Errorf("failed to write LZSS compressed data: %v", err)
|
|
}
|
|
pdata = append(buf.Bytes(), conf.ExtraData...) // Append any extra data after the compressed payload
|
|
case CompressionAlgorithmLZFSE:
|
|
var algo comp.Algorithm
|
|
if strings.ToLower(conf.Compression) == "lzfse_iboot" {
|
|
algo = comp.LZFSE_IBOOT
|
|
} else {
|
|
algo = comp.LZFSE
|
|
}
|
|
compressedData, err := comp.Compress(conf.Data, algo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to %s compress data: %v", conf.Compression, err)
|
|
}
|
|
if len(compressedData) == 0 {
|
|
return nil, fmt.Errorf("failed to %s compress data", conf.Compression)
|
|
}
|
|
utils.Indent(log.Debug, 2)(
|
|
fmt.Sprintf("%s compression: %d → %d bytes (%.1f%% reduction)",
|
|
conf.Compression,
|
|
len(conf.Data), len(compressedData),
|
|
float64(len(conf.Data)-len(compressedData))/float64(len(conf.Data))*100),
|
|
)
|
|
pdata = append(compressedData, conf.ExtraData...)
|
|
default:
|
|
pdata = append(conf.Data, conf.ExtraData...)
|
|
}
|
|
|
|
im4p := &Payload{
|
|
IM4P: IM4P{
|
|
Tag: "IM4P",
|
|
Type: conf.Type,
|
|
Version: conf.Version,
|
|
Data: pdata,
|
|
},
|
|
}
|
|
|
|
// Add the compression block if a compression algorithm was used and there's no extra data.
|
|
if len(conf.ExtraData) == 0 {
|
|
switch compAlgo {
|
|
case CompressionAlgorithmLZSS, CompressionAlgorithmLZFSE:
|
|
im4p.Compression = Compression{
|
|
Algorithm: compAlgo,
|
|
UncompressedSize: len(conf.Data),
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(conf.Keybags) > 0 {
|
|
data, err := asn1.Marshal(conf.Keybags)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal keybags: %v", err)
|
|
}
|
|
im4p.Keybag = data
|
|
im4p.Keybags = conf.Keybags
|
|
im4p.Encrypted = true
|
|
}
|
|
|
|
return im4p, nil
|
|
}
|
|
|
|
/* Payload Processing Functions */
|
|
|
|
// DecryptPayload decrypts an IM4P payload using AES-CBC with provided IV and key
|
|
func DecryptPayload(inputPath, outputPath string, iv, key []byte) error {
|
|
i, err := OpenPayload(inputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to parse IM4P: %v", err)
|
|
}
|
|
|
|
if len(i.Data) < aes.BlockSize {
|
|
return fmt.Errorf("IM4P data too short")
|
|
}
|
|
if len(i.Data)%aes.BlockSize != 0 {
|
|
return fmt.Errorf("IM4P data is not a multiple of the block size")
|
|
}
|
|
if len(iv) != aes.BlockSize {
|
|
return fmt.Errorf("IV must be %d bytes, got %d", aes.BlockSize, len(iv))
|
|
}
|
|
if len(key) == 0 {
|
|
return fmt.Errorf("key cannot be empty")
|
|
}
|
|
|
|
data, err := decryptData(i.Data, iv, key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
i.Data = data
|
|
i.Encrypted = false
|
|
i.Keybag = nil
|
|
i.Keybags = nil
|
|
i.decompressedData = nil
|
|
i.extraData = nil
|
|
|
|
payloadData, err := i.GetData()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get decrypted payload data: %v", err)
|
|
}
|
|
if len(payloadData) == 0 {
|
|
return fmt.Errorf("decrypted payload data is empty")
|
|
}
|
|
|
|
of, err := os.Create(outputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create file %s: %v", outputPath, err)
|
|
}
|
|
defer func() {
|
|
if err := of.Close(); err != nil {
|
|
log.Errorf("failed to close file: %v", err)
|
|
}
|
|
}()
|
|
|
|
if _, err := of.Write(payloadData); err != nil {
|
|
return fmt.Errorf("failed to write decrypted data to file %s: %v", outputPath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func decryptData(data, iv, key []byte) ([]byte, error) {
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create AES cipher: %v", err)
|
|
}
|
|
mode := cipher.NewCBCDecrypter(block, iv)
|
|
mode.CryptBlocks(data, data)
|
|
return data, nil
|
|
}
|