diff --git a/Sources/CLI/Commands/Download.swift b/Sources/CLI/Commands/Download.swift index 13da30d2..e2038312 100644 --- a/Sources/CLI/Commands/Download.swift +++ b/Sources/CLI/Commands/Download.swift @@ -110,7 +110,7 @@ extension Download { "The country provided does not match with the account you are using.", "Supply a valid country using the \"--country\" flag." ].joined(separator: " "), level: .error) - case StoreResponse.Error.passwordTokenExpired: + case StoreResponse.Error.passwordTokenExpired, StoreResponse.Error.passwordChanged: logger.log("Token expired. Login again using the \"auth\" command.", level: .error) default: logger.log("An unknown error has occurred.", level: .error) @@ -164,7 +164,7 @@ extension Download { "The country provided does not match with the account you are using.", "Supply a valid country using the \"--country\" flag." ].joined(separator: " "), level: .error) - case StoreResponse.Error.passwordTokenExpired: + case StoreResponse.Error.passwordTokenExpired, StoreResponse.Error.passwordChanged: logger.log("Token expired. Login again using the \"auth\" command.", level: .error) default: logger.log([ @@ -203,6 +203,30 @@ extension Download { logger.log("Applying patches...", level: .info) try signatureClient.appendMetadata(item: item, email: email) try signatureClient.appendSignature(item: item) + } catch { + switch error { + case SignatureClient.Error.fileNotFound: + logger.log( + "App uses old code signature version, falling back to alternative patching mechanism.", + level: .debug + ) + logger.log("The produced app package may not be compatible with modern iOS releases.", level: .info) + applyOldPatches(item: item, email: email, path: path) + default: + logger.log("\(error)", level: .debug) + logger.log("Failed to apply patches. The ipa file will be left incomplete.", level: .error) + _exit(1) + } + } + } + + private mutating func applyOldPatches(item: StoreResponse.Item, email: String, path: String) { + logger.log("Creating signature client...", level: .debug) + let signatureClient = SignatureClient(fileManager: .default, filePath: path) + + do { + logger.log("Applying fallback patches...", level: .info) + try signatureClient.appendOldSignature(item: item) } catch { logger.log("\(error)", level: .debug) logger.log("Failed to apply patches. The ipa file will be left incomplete.", level: .error) diff --git a/Sources/CLI/Commands/Purchase.swift b/Sources/CLI/Commands/Purchase.swift index cb4c60dc..e6e234a3 100644 --- a/Sources/CLI/Commands/Purchase.swift +++ b/Sources/CLI/Commands/Purchase.swift @@ -103,7 +103,7 @@ extension Purchase { "The country provided does not match with the account you are using.", "Supply a valid country using the \"--country\" flag." ].joined(separator: " "), level: .error) - case StoreResponse.Error.passwordTokenExpired: + case StoreResponse.Error.passwordTokenExpired, StoreResponse.Error.passwordChanged: logger.log("Token expired. Login again using the \"auth\" command.", level: .error) default: logger.log("An unknown error has occurred.", level: .error) diff --git a/Sources/StoreAPI/Signature/SignatureClient.swift b/Sources/StoreAPI/Signature/SignatureClient.swift index 4a355de9..d9b86d34 100644 --- a/Sources/StoreAPI/Signature/SignatureClient.swift +++ b/Sources/StoreAPI/Signature/SignatureClient.swift @@ -51,6 +51,18 @@ public final class SignatureClient: SignatureClientInterface { throw Error.invalidArchive } + try appendSignatureFromManfiest(forItem: item, inArchive: archive) + } + + public func appendOldSignature(item: StoreResponse.Item) throws { + guard let archive = Archive(url: URL(fileURLWithPath: filePath), accessMode: .update) else { + throw Error.invalidArchive + } + + try appendOldSignature(forItem: item, inArchive: archive) + } + + private func appendSignatureFromManfiest(forItem item: StoreResponse.Item, inArchive archive: Archive) throws { let manifest = try readPlist( archive: archive, matchingSuffix: ".app/SC_Info/Manifest.plist", @@ -87,6 +99,52 @@ public final class SignatureClient: SignatureClientInterface { try fileManager.removeItem(at: signatureBaseUrl) } + private func appendOldSignature(forItem item: StoreResponse.Item, inArchive archive: Archive) throws { + guard let infoEntry = archive.first(where: { $0.path.hasSuffix(".app/Info.plist") }) else { + throw Error.invalidAppBundle + } + + let temporaryInfoURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) + _ = try archive.extract(infoEntry, to: temporaryInfoURL, skipCRC32: true) + let infoData = try Data(contentsOf: temporaryInfoURL) + + guard let infoPlist = try PropertyListSerialization.propertyList( + from: infoData, + format: nil + ) as? [String: Any] else { + throw Error.invalidAppBundle + } + + guard let executableName = infoPlist["CFBundleExecutable"] as? String else { + throw Error.invalidAppBundle + } + + let appBundleName = URL(fileURLWithPath: infoEntry.path) + .deletingLastPathComponent() + .deletingPathExtension() + .lastPathComponent + + guard let signatureItem = item.signatures.first(where: { $0.id == 0 }) else { + throw Error.invalidSignature + } + + let signatureBaseUrl = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) + let signatureUrl = signatureBaseUrl + .appendingPathComponent("Payload") + .appendingPathComponent(appBundleName) + .appendingPathExtension("app") + .appendingPathComponent("SC_Info") + .appendingPathComponent(executableName) + .appendingPathExtension("sinf") + + let signatureRelativePath = signatureUrl.path.replacingOccurrences(of: "\(signatureBaseUrl.path)/", with: "") + + try fileManager.createDirectory(at: signatureUrl.deletingLastPathComponent(), withIntermediateDirectories: true) + try signatureItem.sinf.write(to: signatureUrl) + try archive.addEntry(with: signatureRelativePath, relativeTo: signatureBaseUrl) + try fileManager.removeItem(at: signatureBaseUrl) + } + private func readPlist(archive: Archive, matchingSuffix: String, type: T.Type) throws -> T { guard let entry = archive.first(where: { $0.path.hasSuffix(matchingSuffix) }) else { throw Error.fileNotFound(matchingSuffix) @@ -115,7 +173,7 @@ extension SignatureClient { } } - enum Error: Swift.Error { + public enum Error: Swift.Error { case invalidArchive case invalidAppBundle case invalidSignature diff --git a/Sources/StoreAPI/Store/StoreResponse.swift b/Sources/StoreAPI/Store/StoreResponse.swift index 9406feb3..3291fd04 100644 --- a/Sources/StoreAPI/Store/StoreResponse.swift +++ b/Sources/StoreAPI/Store/StoreResponse.swift @@ -15,6 +15,7 @@ public enum StoreResponse { public enum Error: Int, Swift.Error { case unknownError = 0 + case passwordChanged = 2002 case genericError = 5002 case codeRequired = 1 case invalidLicense = 9610