往期文章
寻找IOS相册中相似图片
NSNotification与类对象,实例对象
CocoaPods私有源搭建
雷达扩散效果搜索设备
工具介绍
ipatool 是一个采用swift编写的命令行工具,允许您在 App Store 上搜索 iOS 应用程序并下载应用程序包,需要输入appstoreID来完成应用的授权,
工具作者: majd
工具地址: ipatool
使用效果如下
下面简单的介绍下ipatool各个功能访问的App Store接口以及需要的参数。
搜索模块
搜索功能分为两类,第一类根据名称搜索是模糊搜索,返回的是符合条件的app数组,第二类是根据app的BoundleID进行搜索是一个精确的搜索。
名称搜索
请求
根据输入的app名称,返回符合条件的搜索数组。
功能 | 说明 |
---|---|
host | itunes.apple.com |
请求方式 | get |
接口 | /search |
请求参数
参数 | 作用 |
---|---|
media | media |
term | 搜索app名称 |
limit | 搜索最大限制 |
country | appstore所在的区域 US美国 |
entity | 设备类型 software代表iphone iPadSoftware代表ipad |
响应
结构
{
resultCount:1, //代表返回有多少个搜索结果\
results:[] //搜索结果数组
}
由于返回的参数过多,这里只写了一些有用的字段,读者可以根据自身情况选择有用的字段
字段 | 作用 |
---|---|
trackId | 应用商店唯一标识符 |
trackName | 应用名称 |
bundleId | 应用唯一标识符 |
version | version |
BundleID搜索
请求
根据输入的BoundleId,返回符合条件的搜索数组。
功能 | 说明 |
---|---|
host | itunes.apple.com |
请求方式 | get |
接口 | /lookup |
请求参数
参数 | 作用 |
---|---|
media | software |
bundleId | 应用唯一标识符 |
limit | 1 |
country | appstore所在的区域 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和用户名和密码才能正确返回用户信息
请求
功能 | 说明 |
---|---|
未开启双重认证host | p25-buy.itunes.apple.com |
已开启双重认证host | p71-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()
}
请求头
key | value |
---|---|
User-Agent | Configurator/2.0 (Macintosh; OS X 10.12.6; 16G29) AppleWebKit/2603.3.8 |
Content-Type | application/x-www-form-urlencoded |
请求体
注意这里的不能使用json,不能使用json,不能使用json
要使用Plist,要使用Plist,要使用Plist
这里的code代表安全码,也就是双重认证返回的安全码,如果有的话,尝试次数改为2次,没有的话尝试次数改为4次。
如果有安全码将安全码拼接到密码后面
key | value |
---|---|
appleId | appleID账号 |
attempt | code == nil ? "4" : "2" |
createSession | true |
guid | Mac地址 |
password | password+code??"" |
rmp | 0 |
why | signIn |
响应
key | value |
---|---|
failureType | 错误码 |
directoryServicesIdentifier | 目录授权ID |
account | 用户名 |
获取下载链接
根据应用商店唯一标识符(trackId)与目录授权ID(directoryServicesIdentifier) 获取下载链接
请求
功能 | 说明 |
---|---|
host | p25-buy.itunes.apple.com |
请求方式 | post |
接口 | /WebObjects/MZFinance.woa/wa/volumeStoreDownloadProduct?guid=(guid) |
guid为设备Mac地址
请求头
key | value |
---|---|
User-Agent | Configurator/2.0 (Macintosh; OS X 10.12.6; 16G29) AppleWebKit/2603.3.8 |
Content-Type | application/x-www-form-urlencoded |
X-Dsid | directoryServicesIdentifier |
iCloud-DSID | directoryServicesIdentifier |
请求体
trackId为app在商店的唯一标识符,可以通过搜索获取
key | value |
---|---|
creditDisplay | "" |
guid | mac地址 |
salableAdamId | trackId(应用商店唯一标识符) |
响应
其中signatures是一个数组里面保存着签名二进制文件,需要将这个文件写入到ipa中完成签名
key | value |
---|---|
url | 下载链接 |
md5 | 校验 |
metadata | iTunesMetadata.plist |
signatures | 签名数组 |
签名
目录结构
通过对比已经签名和未签名ipa目录,发现已经签名ipa目录多了两个文件,一个是iTunesMetadata.plist,一个是PayLoad目录里SC_Info目录下的MBackupper.sinf文件。所以我们只要上一步中的metadata转换成iTunesMetadata.plist,并将signatures数组里面第一个签名文件写到SC_Info文件夹下。
未签名ipa
已签名ipa
原理
Item为download中获取的下载信息。
其中保存了Medata用于生成ipa(安装包)里面的iTunesMetadata.plist
Signatures:保存着签名文件数组,一般使用$0,放入SC_info目录中
安装
成功完成签名之后,可以使用命令行工具ideviceinstaller将签好名的ipa推送到设备上完成安装。
ideviceinstaller -i xxx
结束
如果你觉得我分析的还不错,请给我点个赞哦。