IPA下载工具原理解析

3,718 阅读4分钟

往期文章

寻找IOS相册中相似图片
NSNotification与类对象,实例对象
CocoaPods私有源搭建
雷达扩散效果搜索设备

工具介绍

ipatool 是一个采用swift编写的命令行工具,允许您在 App Store 上搜索 iOS 应用程序并下载应用程序包,需要输入appstoreID来完成应用的授权,

工具作者: majd

工具地址: ipatool

使用效果如下

image.png

下面简单的介绍下ipatool各个功能访问的App Store接口以及需要的参数。

搜索模块

搜索功能分为两类,第一类根据名称搜索是模糊搜索,返回的是符合条件的app数组,第二类是根据app的BoundleID进行搜索是一个精确的搜索。

名称搜索

请求

根据输入的app名称,返回符合条件的搜索数组。

功能说明
hostitunes.apple.com
请求方式get
接口/search

请求参数

参数作用
mediamedia
term搜索app名称
limit搜索最大限制
countryappstore所在的区域 US美国
entity设备类型 software代表iphone iPadSoftware代表ipad

响应

结构

{
resultCount:1, //代表返回有多少个搜索结果\
results:[] //搜索结果数组
}

由于返回的参数过多,这里只写了一些有用的字段,读者可以根据自身情况选择有用的字段

字段作用
trackId应用商店唯一标识符
trackName应用名称
bundleId应用唯一标识符
versionversion

BundleID搜索

请求

根据输入的BoundleId,返回符合条件的搜索数组。

功能说明
hostitunes.apple.com
请求方式get
接口/lookup

请求参数

参数作用
mediasoftware
bundleId应用唯一标识符
limit1
countryappstore所在的区域 US美国
entity设备类型 software代表iphone iPadSoftware代表ipad

响应

返回的格式与名称搜索结构是一样的,所以请参考名称搜索的返回结构

下载模块

下载模块的流程如下

graph TD
 A[开始]

A --> B{检测是否获取授权码}

B -->|获取授权码| D[开始下载]

D --> H

B -->|未取授权码| E[提醒用户输入用户名和密码]

E --> F{是否开启双重认证}

F --> |是| G[输入安全码]

F --> |否| I[请求权限获取授权码]

G --> J[将安全码带入请求获取授权码]

I --> H[通过授权码访问指定app的下载链接以及签名文件]

J --> H

H --> M[使用下载链接将APP下载的指定路径]

M --> N[使用签名文件签名下载好的ipa]

N --> 结束

定义的错误码statusCode如下

enum Error: Int, Swift.Error {

      case unknownError = 0             //无效响应

      case genericError = 5002

      case codeRequired = 1             //需要2FA认证

      case invalidLicense = 9610        //Apple ID 没有此应用程序的许可

      case invalidCredentials = -5000    //无效证书

      case invalidAccount = 5001         //此 Apple ID 尚未设置为使用 App Store

      case invalidItem = -10000         //无效应用

      case lockedAccount = -10001       //出于安全原因,此 Apple ID 已被禁用
}

授权

根据用户名和密码授权访问商店,注意这里authenticate方法会进行两次请求,

第一次请求isFirstAttept为true,代表如果请求返回 -5000(invalidCredentials)。则进行第二次请求

func authenticate(email: String, password: String, code: String?, completion: @escaping (Result<StoreResponse.Account, Swift.Error>) -> Void) {

        authenticate(email: email,
                     password: password,
                     code: code,
                     isFirstAttempt: true,
                     completion: completion)
    }

返回-5000进行第二次请求

case StoreResponse.Error.invalidCredentials:

                 if isFirstAttempt {
                      return self?.authenticate(email: email,
                                                password: password,
                                                code: code,
                                                isFirstAttempt: false,
                                                completion: completion) ?? ()
              }

这是因为请求授权用户数据接口,如果没有带上认证cookie,那么即使用户输入正确的账户和密码也不能成功登陆,只会返回-5000 。必须要带上认证cookie。而当第一次使用authenticate接口,带上用户名和密码时,就会返回对于的认证cookie,在下一次调用authenticate接口带上用户名和密码就能成功获取到用户的数据。

所以这里需要对authenticate接口进行两次请求,第一次请求获取认证cookie并且保存起来,第二次请求认证接口带上认证cookie和用户名和密码才能正确返回用户信息

请求

功能说明
未开启双重认证hostp25-buy.itunes.apple.com
已开启双重认证hostp71-buy.itunes.apple.com
请求方式post
接口/WebObjects/MZFinance.woa/wa/authenticate?guid=(guid)

guid为设备的Mac地址

这里分享一个获取Mac地址的方法

func guid() -> String {

    let MAC_ADDRESS_LENGTH = 6

    let bsds: [String] = ["en0", "en1"]

    var bsd: String = bsds[0]
    
    var length : size_t = 0

    var buffer : [CChar]
    
    var bsdIndex = Int32(if_nametoindex(bsd))

    if bsdIndex == 0 {

        bsd = bsds[1]

        bsdIndex = Int32(if_nametoindex(bsd))

        guard bsdIndex != 0 else { fatalError("Could not read MAC address") }

    }
    let bsdData = Data(bsd.utf8)

    var managementInfoBase = [CTL_NET, AF_ROUTE, 0, AF_LINK, NET_RT_IFLIST, bsdIndex]
    
    guard sysctl(&managementInfoBase, 6, nil, &length, nil, 0) >= 0 else { fatalError("Could not read MAC address") }

    buffer = [CChar](unsafeUninitializedCapacity: length, initializingWith: {buffer, initializedCount in

        for x in 0..<length { buffer[x] = 0 }

        initializedCount = length

    })
    guard sysctl(&managementInfoBase, 6, &buffer, &length, nil, 0) >= 0 else { fatalError("Could not read MAC address") }

    let infoData = Data(bytes: buffer, count: length)

    let indexAfterMsghdr = MemoryLayout<if_msghdr>.stride + 1

    let rangeOfToken = infoData[indexAfterMsghdr...].range(of: bsdData)!

    let lower = rangeOfToken.upperBound

    let upper = lower + MAC_ADDRESS_LENGTH

    let macAddressData = infoData[lower..<upper]

    let addressBytes = macAddressData.map{ String(format:"%02x", $0) }

    return addressBytes.joined().uppercased()

}

请求头

keyvalue
User-AgentConfigurator/2.0 (Macintosh; OS X 10.12.6; 16G29) AppleWebKit/2603.3.8
Content-Typeapplication/x-www-form-urlencoded

请求体

注意这里的不能使用json,不能使用json,不能使用json

要使用Plist,要使用Plist,要使用Plist

这里的code代表安全码,也就是双重认证返回的安全码,如果有的话,尝试次数改为2次,没有的话尝试次数改为4次。

如果有安全码将安全码拼接到密码后面

keyvalue
appleIdappleID账号
attemptcode == nil ? "4" : "2"
createSessiontrue
guidMac地址
passwordpassword+code??""
rmp0
whysignIn

响应

keyvalue
failureType错误码
directoryServicesIdentifier目录授权ID
account用户名

获取下载链接

根据应用商店唯一标识符(trackId)与目录授权ID(directoryServicesIdentifier) 获取下载链接

请求

功能说明
hostp25-buy.itunes.apple.com
请求方式post
接口/WebObjects/MZFinance.woa/wa/volumeStoreDownloadProduct?guid=(guid)

guid为设备Mac地址

请求头

keyvalue
User-AgentConfigurator/2.0 (Macintosh; OS X 10.12.6; 16G29) AppleWebKit/2603.3.8
Content-Typeapplication/x-www-form-urlencoded
X-DsiddirectoryServicesIdentifier
iCloud-DSIDdirectoryServicesIdentifier

请求体

trackId为app在商店的唯一标识符,可以通过搜索获取

keyvalue
creditDisplay""
guidmac地址
salableAdamIdtrackId(应用商店唯一标识符)

响应

其中signatures是一个数组里面保存着签名二进制文件,需要将这个文件写入到ipa中完成签名

keyvalue
url下载链接
md5校验
metadataiTunesMetadata.plist
signatures签名数组

签名

目录结构

通过对比已经签名和未签名ipa目录,发现已经签名ipa目录多了两个文件,一个是iTunesMetadata.plist,一个是PayLoad目录里SC_Info目录下的MBackupper.sinf文件。所以我们只要上一步中的metadata转换成iTunesMetadata.plist,并将signatures数组里面第一个签名文件写到SC_Info文件夹下。

未签名ipa

image.png

已签名ipa

image.png

原理

Item为download中获取的下载信息。

其中保存了Medata用于生成ipa(安装包)里面的iTunesMetadata.plist

Signatures:保存着签名文件数组,一般使用$0,放入SC_info目录中

image.png

安装

成功完成签名之后,可以使用命令行工具ideviceinstaller将签好名的ipa推送到设备上完成安装。

ideviceinstaller -i xxx

结束

如果你觉得我分析的还不错,请给我点个赞哦。