Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Patch old code signature revisions as fallback #94

Merged
merged 1 commit into from Jul 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 26 additions & 2 deletions Sources/CLI/Commands/Download.swift
Expand Up @@ -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)
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Sources/CLI/Commands/Purchase.swift
Expand Up @@ -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)
Expand Down
60 changes: 59 additions & 1 deletion Sources/StoreAPI/Signature/SignatureClient.swift
Expand Up @@ -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",
Expand Down Expand Up @@ -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<T: Decodable>(archive: Archive, matchingSuffix: String, type: T.Type) throws -> T {
guard let entry = archive.first(where: { $0.path.hasSuffix(matchingSuffix) }) else {
throw Error.fileNotFound(matchingSuffix)
Expand Down Expand Up @@ -115,7 +173,7 @@ extension SignatureClient {
}
}

enum Error: Swift.Error {
public enum Error: Swift.Error {
case invalidArchive
case invalidAppBundle
case invalidSignature
Expand Down
1 change: 1 addition & 0 deletions Sources/StoreAPI/Store/StoreResponse.swift
Expand Up @@ -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
Expand Down