ios重签名教程
2017-08-20 21:40
381 查看
iOS自动打包(重签名的方式)–用Mac客户端来实现
公司最近出了个需求,要求迅速给客户打一些马甲包,就是替换里面的plist和一些资源文件(icon和launchImage),于是找了很多资料,发现这一部分很多内容都已过期或者说讲的不全面,遂收集了一个全套的ipa重签名内容,分享给大家。代码是用swift写的,版本3.0常量定义
struct PathDefine { static let UnzipPath = NSTemporaryDirectory().appending("unzip") static let TargetPath = "/Users/apple/Desktop" static let outputPath : String = "/Users/apple/Desktop/autoPackage" } struct OtherStringDefine { static let appid = "application-identifier" static let kTeamIdentifier = "com.apple.developer.team-identifier" static let kKeychainAccessGroups = "keychain-access-groups" static let codeSignature = "_CodeSignature" static let dbVersionKey = "dbVersion" }
这些常量在以下内容中会用到
1解包
//解压之前先创建解压目录 fileprivate func createWorkingPath() { let manager = FileManager.default if manager.fileExists(atPath: PathDefine.UnzipPath) == false { //不存在目录时创建一个 do { try manager.createDirectory(atPath: PathDefine.UnzipPath, withIntermediateDirectories: true, attributes: nil) } catch { print(error.localizedDescription) } } else { // 存在目录时清空目录 do { try manager.removeItem(atPath: PathDefine.UnzipPath) } catch { print(error.localizedDescription) } //再创建一个 do { try manager.createDirectory(atPath: PathDefine.UnzipPath, withIntermediateDirectories: true, attributes: nil) } catch { print(error.localizedDescription) } } } //这两个方法也是会多次被调用的,作用是监听task运行结果 fileprivate func checkComplete(task : Process, complete : @escaping (Bool) -> Void) { Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(timerComplete(sender:)), userInfo: ["task" : task, "complete" : complete], repeats: true) } @objc private func timerComplete(sender : Timer) { let task : Process = (sender.userInfo as! Dictionary<String, Any>)["task"] as! Process let complete : (Bool) -> Void = (sender.userInfo as! Dictionary<String, Any>)["complete"] as! ((Bool) -> Void) if task.isRunning == false { sender.invalidate() if task.terminationStatus == 1 { complete(false) } else { complete(true) } } } //开始解包 fileprivate func unzip(complete :@escaping (Bool) -> Void) { let task = Process.init() task.launchPath = "/usr/bin/unzip" //这个record.ipaInputPath是我模型中的变量,ipa母包的绝对路径 大家可以把ipa丢到终端里面,就可以看到了 task.arguments = [ "-q", record.ipaInputPath, "-d", PathDefine.UnzipPath] let pi = Pipe.init() task.standardOutput = pi let file = pi.fileHandleForReading task.launch() let data = file.readDataToEndOfFile() print(String.init(data: data, encoding: .utf8) ?? "") self.checkComplete(task: task) { [unowned self] (result) in self.removeCodeSignature() //移除签名,内容在下面 complete(result) } }
2移除签名文件
为什么要移除签名文件,因为我们这个是二次签名,以前的签名文件肯定不能用了啊,而且如果不移除了会影响签名过程fileprivate func removeCodeSignature() { var appName = self.record.ipaInputPath appName = appName.substring(to: appName.index(appName.endIndex, offsetBy: -4)) //这里要说一下,解包后的目录Payload/下.app文件的文件名默认是你xcode里面被打包那个target的名字。因为我的工程名和target名不一样,所有要替换一下。 targetName变量就是我的target名字 if targetName.characters.count > 0 { appName = targetName } let appPath = PathDefine.UnzipPath + "/Payload/" + URL.init(fileURLWithPath: appName).lastPathComponent + ".app" let codeSignPath = appPath + "/" + OtherStringDefine.codeSignature let manager = FileManager.default if manager.fileExists(atPath: codeSignPath) { do { try manager.removeItem(atPath: codeSignPath) } catch { print(error.localizedDescription) } } }
3 编辑plist文件
fileprivate func editPlist(complete : (Bool) -> Void) { var appName = record.ipaInputPath appName = appName.substring(to: appName.index(appName.endIndex, offsetBy: -4)) if targetName.characters.count > 0 { appName = targetName } let plistPath = PathDefine.UnzipPath + "/Payload/" + URL.init(fileURLWithPath: appName 4000 ).lastPathComponent + ".app/Info.plist" let plistDic = NSMutableDictionary.init(contentsOfFile: plistPath) if plistDic == nil { complete(false) return } //定义了一个delegate变量,因为我要打多个不同项目的包,所以他的delegate来实现不同的编辑逻辑,如果不知道的plist文件中每个key的名字,可以在这里把plistDic打印到控制台上 self.delegate?.editPlist(plistDic: plistDic!) //移除之前的plist文件 let manager = FileManager.default do { try manager.removeItem(atPath: plistPath) } catch { print(error.localizedDescription) } //把plist文件按照XML格式重新写入到目录下 do { let xmlData = try PropertyListSerialization.data(fromPropertyList: plistDic ?? "", format: .xml, options: 0) as NSData if xmlData.write(toFile: plistPath, atomically: true) == false { complete(false) return } } catch { print(error.localizedDescription) } complete(true) }
4替换icon和launchImage
fileprivate func replaceIconAndLaunchImage(complete :@escaping (Bool) -> Void) { //这里每个被替换的图片的名字都是你原来工程里面配置的名字,比如AppIcon29x29@2x.png,AppIcon就是我在Xcode-General-App icons And Launch Images-App Icons Source中配置的名字,launchImage同理 let destinationNameArray = ["AppIcon29x29@2x.png", "AppIcon29x29@3x.png", "AppIcon40x40@2x.png", "AppIcon40x40@3x.png", "AppIcon60x60@2x.png", "AppIcon60x60@3x.png", "LaunchImage-700@2x.png", "LaunchImage-700-568h@2x.png", "LaunchImage-800-667h@2x.png", "LaunchImage-800-Portrait-736h@3x.png"] let sourcePathArray = [record.icon58Path, record.icon87Path, record.icon80Path, record.icon120Path, record.icon120_2Path, record.icon180Path, record.launch4Path, record.launch5Path, record.launch6Path, record.launch6pPath] for index in 0..<sourcePathArray.count { let manager = FileManager.default var appName = record.ipaInputPath appName = appName.substring(to: appName.index(appName.endIndex, offsetBy: -4)) if targetName.characters.count > 0 { appName = targetName } let payloadPath = PathDefine.UnzipPath + "/Payload/" + URL.init(fileURLWithPath: appName).lastPathComponent + ".app" let destinationPath = payloadPath + "/" + destinationNameArray[index] if manager.fileExists(atPath: destinationPath) { do { try manager.removeItem(atPath: destinationPath) } catch { complete(false) print(error.localizedDescription) } do { try manager.copyItem(atPath: sourcePathArray[index], toPath: destinationPath) } catch { complete(false) print(error.localizedDescription) } } } complete(true) }
5编辑entitlement文件
entitlement文件文件里面存放了和签名有关的内容,是通过当前这个provisionFile文件和plist中的相关内容生成的private func createEntitlements(_ complete: @escaping (Bool) -> Void) { //删除entitlement文件 let entitlmentPath = PathDefine.UnzipPath + "/Entitlements.plist" let manager = FileManager.default if manager.fileExists(atPath: entitlmentPath) { do { try manager.removeItem(atPath: entitlmentPath) } catch { print(error.localizedDescription) } } let task = Process.init() task.launchPath = "/usr/bin/security" //record.provisionPath是我模型中ProvisonFile的路径,你可以替换你的ProvisionFile的绝对路径。但是,但是,但是,你plist中Bundle Identifier的值必须和你ProvisionFile指定的id相同,比如你ProvisionFile指定的id是com.366EC.test,那么你的plist中Bundle Identifier也必须是这个值,否则通不过security过程。另外ProvisionFile类型最好是发布类型,开发类型的我没测试过 task.arguments = ["cms", "-D", "-i", record.provisionPath] task.currentDirectoryPath = PathDefine.UnzipPath let pi = Pipe.init() task.standardOutput = pi let file = pi.fileHandleForReading task.launch() self.checkComplete(task: task) { [unowned self] (result) in if result == false { let errorString = String.init(data: file.readDataToEndOfFile(), encoding: .utf8) ?? "" complete(false) } else { //这个地方会得到一个security过的字符串,但是在Mac OS 10.10以上会报出“SecPolicySetValue”打头的一句话,这个必须去掉。此时调试台也会打印“security: SecPolicySetValue: One or more parameters passed to a function were not valid.”那是正常的,不管它 let data = file.readDataToEndOfFile() var entitlementsResult = String.init(data: data, encoding: .ascii) ?? "" if entitlementsResult.contains("SecPolicySetValue") { var inOutput : Array<String> = entitlementsResult.components(separatedBy: "\n") inOutput.remove(at: 0) entitlementsResult = inOutput.joined(separator: "\n") } let entitlementData = entitlementsResult.data(using: .utf8)! do { var entitlementDic = try PropertyListSerialization.propertyList(from: entitlementData, options: PropertyListSerialization.ReadOptions.mutableContainers, format: nil) as! Dictionary<String, Any> entitlementDic = entitlementDic["Entitlements"] as! Dictionary<String, Any> //entitlementDic中有一个key为OtherStringDefine.appid的字段必须改为"teamid.bundlId"这种格式,teamID就是你证书后面括号中那一串,没有括号的去你的开发者帐号里面查,每个证书都有一个teamID entitlementDic.updateValue(String.init(format: "%@.%@", self.record.teamId, self.record.bid), forKey: OtherStringDefine.appid) //移除无用的字段 entitlementDic.removeValue(forKey: OtherStringDefine.kTeamIdentifier) entitlementDic.removeValue(forKey: OtherStringDefine.kKeychainAccessGroups) //和plist文件一样,转换为XML文件重新写入原来的目录中 do { let xmlData = try PropertyListSerialization.data(fromPropertyList: entitlementDic, format: .xml, options: 0) as NSData if xmlData.write(toFile: entitlmentPath, atomically: true) == false { complete(false) return } } catch { print(error.localizedDescription) } } catch { print(error.localizedDescription) } complete(true) } } }
6替换Provision文件
这里要注意一下,放入打包目录里面的provision文件的名字必须是embedded.mobileprovisionprivate func editProvisionFile(_ complete: @escaping (Bool) -> Void) { //替换provisioning let manager = FileManager.default var appName = record.ipaInputPath appName = appName.substring(to: appName.index(appName.endIndex, offsetBy: -4)) if targetName.characters.count > 0 { appName = targetName } let payloadPath = PathDefine.UnzipPath + "/Payload/" + URL.init(fileURLWithPath: appName).lastPathComponent + ".app" let appProfilePath = payloadPath + "/embedded.mobileprovision" if manager.fileExists(atPath: appProfilePath) { do { try manager.removeItem(atPath: appProfilePath) } catch { print(error.localizedDescription) } } //这里用了Process来做拷贝,其实用FileManager也是一样的,我之前遇到一些问题,总以为是因为使用FileManager造成的权限问题,但最后通过对比实验,发现不是这里造成的 let task = Process.init() task.launchPath = "/bin/cp" task.arguments = [record.provisionPath, appProfilePath] let pi = Pipe.init() task.standardOutput = pi let file = pi.fileHandleForReading task.launch() let data = file.readDataToEndOfFile() print(String.init(data: data, encoding: .utf8) ?? "") self.checkComplete(task: task) { (result) in complete(result) } }
7重签名
// 在.app目录会有一些图片缓存,必须清理一下,否则不能通过签名 private func removeUnrealizePath(_ complete: @escaping (Bool) -> Void) { let task = Process.init() task.launchPath = "/usr/bin/xattr" var appName = record.ipaInputPath appName = appName.substring(to: appName.index(appName.endIndex, offsetBy: -4)) if targetName.characters.count > 0 { appName = targetName } let appPath = PathDefine.UnzipPath + "/Payload/" + URL.init(fileURLWithPath: appName).lastPathComponent + ".app" task.arguments = [ "-cr", appPath] // task.currentDirectoryPath = appPath let pi = Pipe.init() task.standardOutput = pi let file = pi.fileHandleForReading task.launch() let data = file.readDataToEndOfFile() // print(String.init(data: data, encoding: .utf8)) self.checkComplete(task: task) { (result) in complete(result) } } private func doCodeSign(_ complete: @escaping (Bool) -> Void) { self.removeUnrealizePath { [unowned self] (result) in var appName = self.record.ipaInputPath appName = appName.substring(to: appName.index(appName.endIndex, offsetBy: -4)) if self.targetName.characters.count > 0 { appName = self.targetName } let appPath = PathDefine.UnzipPath + "/Payload/" + URL.init(fileURLWithPath: appName).lastPathComponent + ".app" var task = Process.init() task.launchPath = "/bin/sh" task.currentDirectoryPath = PathDefine.UnzipPath //这里是在解压目录下寻找需要加签的文件,并放入directories.txt中。这里用;\n来分隔连续执行的命令,用\n来分隔一条命令中需要换行的部分 var cmdString = "find -d " + PathDefine.UnzipPath + " \\( -name \"*.app\" -o -name \"*.appex\" -o -name \"*.framewo a38f rk\" -o -name \"*.dylib\" \\) > directories.txt;\n" //把刚才那个directories.txt中得到的文件名字取出来,依次签名。self.record.certificate是你证书的名字,不是证书路径,在钥匙串里面选中证书后点显示简介就能看到 cmdString = cmdString.appendingFormat("while IFS='' read -r line || [[ -n \"$line\" ]]; do \n /usr/bin/codesign --continue -f -s \"%@\" --entitlements \"Entitlements.plist\" \"$line\" \n done < directories.txt", self.record.certificate) //第一个参数"-c"的意思是说后面整个字符串都是命令 task.arguments = ["-c", cmdString] let pi = Pipe.init() task.standardOutput = pi let file = pi.fileHandleForReading task.launch() let data = file.readDataToEndOfFile() self.checkComplete(task: task) { [unowned self] (result) in if result == false { let errorString = String.init(data: file.readDataToEndOfFile(), encoding: .utf8) ?? "" complete(result) } else { complete(result) } } } }
在这个过程中会遇到你证书冲突的情况,你看看你钥匙串里面在“登录”或者“系统”里面是不是有相同的证书,删掉同名的就行了,另外如果证书过期了也会造成签名失败。如果没有执行removeUnrealizePath()也会出现提示你证书冲突。
我之前看过一些教程,仅对.app文件进行了签名,但是里面的动态库、framework、pluging都没有加签,造成了虽然签名成功,但是无法安装的情况
8打包
fileprivate func zipAction(_ complete : @escaping (Bool) -> Void) { let task = Process.init() let ipaOutputPath = PathDefine.outputPath + "/" + record.appName + ".ipa" task.launchPath = "/usr/bin/zip" task.arguments = ["-qry", ipaOutputPath, "Payload/"] task.currentDirectoryPath = PathDefine.UnzipPath let pi = Pipe.init() task.standardOutput = pi let file = pi.fileHandleForReading task.launch() let data = file.readDataToEndOfFile() print(String.init(data: data, encoding: .utf8) ?? "") self.checkComplete(task: task) { [unowned self] (result) in if result == false { complete(false) } else { //这里就是把解包的目录删了 self.clear({ (result) in if result == false { print("清理缓存失败") } complete(true) }) } } }
如果所有的命令都执行完了,并且都成功了,那么可把find调出来并打开输出的目录
NSWorkspace.shared().openFile(PathDefine.outputPath, withApplication: "Finder")
写在最后,整理这个重新打包的教程花了我一个周的时间,其中有的时间花在了制作Mac客户端的界面和学习shell命令上,而且在使用的Process的时候也遇到很多问题,希望广大coder在参考我代码时候一定要小心。文中出现了一些外面传进来的变量,比如record变量,它的属性名的字面意思已经再清楚不过了,这里就不再一一说明
相关文章推荐
- 官方recovery签名验证的破解教程
- CSS基础教程十九之CSS图文混排,图像签名,多图拼接和图片特效
- 重签名教程
- [Android Studio 权威教程]打包、生成jks密钥、签名Apk、多渠道打包
- C#微信公众号开发系列教程三(消息体签名及加解密)
- PhotoShop(PS)设计打造中国风动漫签名图实例教程
- 比特币开发知识(电脑教程和数字消息签名)
- [Android Studio 权威教程]多渠道打包和一键完成(全部产品)打包并签名(转载)
- 使用GnuPG(PGP)加密信息及数字签名教程
- 签署您的应用——多渠道签名打包教程
- [Android Studio 权威教程]多渠道打包和一键完成(全部产品)打包并签名
- 安卓项目eclipse实用教程:设置应用名字和图标、屏幕、签名、真机调试、clean、logcat、json解析
- 【教程】如何给ROM签名
- Android基础入门教程——1.9 Android程序签名打包
- [Android Studio 权威教程]多渠道打包和一键完成(全部产品)打包并签名
- 使用GnuPG(PGP)加密信息及数字签名教程
- 诺基亚手机S60系统证书申请、软件签名图文教程
- 【教程】如何给ROM签名
- 【教程】利用幸运破解器去除安卓签名验证