From 244b4bab5d6143b095ec2be81fb839d9b1b3bbd3 Mon Sep 17 00:00:00 2001 From: Mineek <84083936+mineek@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:16:15 +0100 Subject: [PATCH] Add actual code. --- .gitignore | 3 + Makefile | 24 + MuffinStoreJailed.xcodeproj/project.pbxproj | 65 ++- .../xcshareddata/swiftpm/Package.resolved | 42 ++ MuffinStoreJailed/ContentView.swift | 163 +++++- MuffinStoreJailed/Downgrader.swift | 131 +++++ MuffinStoreJailed/IPATool.swift | 510 ++++++++++++++++++ README.md | 5 + 8 files changed, 933 insertions(+), 10 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 MuffinStoreJailed.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 MuffinStoreJailed/Downgrader.swift create mode 100644 MuffinStoreJailed/IPATool.swift create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6ba3b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +.DS_Store +*.ipa \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dd46b91 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +BASEDIR = $(shell pwd) +BUILD_DIR = $(BASEDIR)/build +INSTALL_DIR = $(BUILD_DIR)/install +PROJECT = $(BASEDIR)/MuffinStoreJailed.xcodeproj +SCHEME = MuffinStoreJailed +CONFIGURATION = Release +SDK = iphoneos +DERIVED_DATA_PATH = $(BUILD_DIR) + +all: ipa + +ipa: + mkdir -p ./build + xcodebuild -jobs 8 -project $(PROJECT) -scheme $(SCHEME) -configuration $(CONFIGURATION) -sdk $(SDK) -derivedDataPath $(DERIVED_DATA_PATH) CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES=NO DSTROOT=$(INSTALL_DIR) + rm -rf ./build/MuffinStoreJailed.ipa + rm -rf ./build/Payload + mkdir -p ./build/Payload + cp -rv ./build/Build/Products/Release-iphoneos/MuffinStoreJailed.app ./build/Payload + cd ./build && zip -r MuffinStoreJailed.ipa Payload + mv ./build/MuffinStoreJailed.ipa ./ + +clean: + rm -rf ./build + rm -rf ./MuffinStoreJailed.ipa diff --git a/MuffinStoreJailed.xcodeproj/project.pbxproj b/MuffinStoreJailed.xcodeproj/project.pbxproj index becbf35..6153ea1 100644 --- a/MuffinStoreJailed.xcodeproj/project.pbxproj +++ b/MuffinStoreJailed.xcodeproj/project.pbxproj @@ -6,6 +6,11 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + 6FA2A8FF2D2426F2005CF73D /* Telegraph in Frameworks */ = {isa = PBXBuildFile; productRef = 6FA2A8FE2D2426F2005CF73D /* Telegraph */; }; + 6FA2A9022D2426F9005CF73D /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = 6FA2A9012D2426F9005CF73D /* Zip */; }; +/* End PBXBuildFile section */ + /* Begin PBXFileReference section */ 6FA2A8E82D24268F005CF73D /* MuffinStoreJailed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MuffinStoreJailed.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -23,6 +28,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6FA2A9022D2426F9005CF73D /* Zip in Frameworks */, + 6FA2A8FF2D2426F2005CF73D /* Telegraph in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -65,6 +72,8 @@ ); name = MuffinStoreJailed; packageProductDependencies = ( + 6FA2A8FE2D2426F2005CF73D /* Telegraph */, + 6FA2A9012D2426F9005CF73D /* Zip */, ); productName = MuffinStoreJailed; productReference = 6FA2A8E82D24268F005CF73D /* MuffinStoreJailed.app */; @@ -94,6 +103,10 @@ ); mainGroup = 6FA2A8DF2D24268F005CF73D; minimizedProjectReferenceProxies = 1; + packageReferences = ( + 6FA2A8FD2D2426F2005CF73D /* XCRemoteSwiftPackageReference "Telegraph" */, + 6FA2A9002D2426F9005CF73D /* XCRemoteSwiftPackageReference "Zip" */, + ); preferredProjectObjectVersion = 77; productRefGroup = 6FA2A8E92D24268F005CF73D /* Products */; projectDirPath = ""; @@ -255,11 +268,13 @@ DEVELOPMENT_TEAM = K9BN25527C; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSFaceIDUsageDescription = "To securely store your Apple ID authentication information, biometric authentication is required."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -267,6 +282,10 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = dev.mineek.MuffinStoreJailed; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -284,11 +303,13 @@ DEVELOPMENT_TEAM = K9BN25527C; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSFaceIDUsageDescription = "To securely store your Apple ID authentication information, biometric authentication is required."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -296,6 +317,10 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = dev.mineek.MuffinStoreJailed; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -324,6 +349,38 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 6FA2A8FD2D2426F2005CF73D /* XCRemoteSwiftPackageReference "Telegraph" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Building42/Telegraph"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.40.0; + }; + }; + 6FA2A9002D2426F9005CF73D /* XCRemoteSwiftPackageReference "Zip" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/marmelroy/Zip.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.1.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 6FA2A8FE2D2426F2005CF73D /* Telegraph */ = { + isa = XCSwiftPackageProductDependency; + package = 6FA2A8FD2D2426F2005CF73D /* XCRemoteSwiftPackageReference "Telegraph" */; + productName = Telegraph; + }; + 6FA2A9012D2426F9005CF73D /* Zip */ = { + isa = XCSwiftPackageProductDependency; + package = 6FA2A9002D2426F9005CF73D /* XCRemoteSwiftPackageReference "Zip" */; + productName = Zip; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 6FA2A8E02D24268F005CF73D /* Project object */; } diff --git a/MuffinStoreJailed.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/MuffinStoreJailed.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..8edc320 --- /dev/null +++ b/MuffinStoreJailed.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,42 @@ +{ + "originHash" : "d4c4b77f172c05b4c915f71e1c7c6770ebca98c66f1fca80b3c7f15502e3af78", + "pins" : [ + { + "identity" : "cocoaasyncsocket", + "kind" : "remoteSourceControl", + "location" : "https://github.com/robbiehanson/CocoaAsyncSocket.git", + "state" : { + "revision" : "dbdc00669c1ced63b27c3c5f052ee4d28f10150c", + "version" : "7.6.5" + } + }, + { + "identity" : "httpparserc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Building42/HTTPParserC.git", + "state" : { + "revision" : "a32b391977a17c30fceec0f38933a359dfdaf112", + "version" : "9.2.0" + } + }, + { + "identity" : "telegraph", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Building42/Telegraph", + "state" : { + "revision" : "f56b6726195e271fea397b7dd11c4adc36d5a32d", + "version" : "0.40.0" + } + }, + { + "identity" : "zip", + "kind" : "remoteSourceControl", + "location" : "https://github.com/marmelroy/Zip.git", + "state" : { + "revision" : "67fa55813b9e7b3b9acee9c0ae501def28746d76", + "version" : "2.1.2" + } + } + ], + "version" : 3 +} diff --git a/MuffinStoreJailed/ContentView.swift b/MuffinStoreJailed/ContentView.swift index 8bb8d71..3e06eae 100644 --- a/MuffinStoreJailed/ContentView.swift +++ b/MuffinStoreJailed/ContentView.swift @@ -2,20 +2,171 @@ // ContentView.swift // MuffinStoreJailed // -// Created by Mineek on 31/12/2024. +// Created by Mineek on 26/12/2024. // import SwiftUI -struct ContentView: View { +struct HeaderView: View { var body: some View { VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") + Text("MuffinStore Jailed") + .font(.largeTitle) + .fontWeight(.bold) + Text("by @mineekdev") + .font(.caption) + } + } +} + +struct FooterView: View { + var body: some View { + VStack { + VStack { + Image(systemName: "exclamationmark.triangle") + .foregroundStyle(.red) + Text("Use at your own risk!") + .foregroundStyle(.yellow) + Image(systemName: "exclamationmark.triangle") + .foregroundStyle(.red) + } + Text("I am not responsible for any damage, data loss, or any other issues caused by using this tool.") + .font(.caption) + } + } +} + +struct ContentView: View { + @State var ipaTool: IPATool? + + @State var appleId: String = "" + @State var password: String = "" + @State var code: String = "" + + @State var isAuthenticated: Bool = false + @State var isDowngrading: Bool = false + + @State var appLink: String = "" + + var body: some View { + VStack { + HeaderView() + Spacer() + if !isAuthenticated { + VStack { + Text("Log in to the App Store") + .font(.headline) + .fontWeight(.bold) + Text("Your credentials will be sent directly to Apple.") + .font(.caption) + } + TextField("Apple ID", text: $appleId) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding() + .autocapitalization(.none) + .disableAutocorrection(true) + SecureField("Password", text: $password) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding() + TextField("2FA Code", text: $code) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding() + Button("Authenticate") { + if appleId.isEmpty || password.isEmpty || code.isEmpty { + return + } + let finalPassword = password + code + ipaTool = IPATool(appleId: appleId, password: finalPassword) + let ret = ipaTool?.authenticate() + isAuthenticated = ret ?? false + } + .padding() + + HStack { + Image(systemName: "info.circle.fill") + .foregroundColor(.yellow) + Text("You WILL need to give a 2FA code to successfully log in.") + } + } else { + if isDowngrading { + VStack { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + Text("Please wait...") + .font(.headline) + .fontWeight(.bold) + Text("The app is being downgraded. This may take a while.") + .font(.caption) + + Button("Done (exit app)") { + exit(0) // scuffed + } + .padding() + } + } else { + VStack { + Text("Downgrade an app") + .font(.headline) + .fontWeight(.bold) + Text("Enter the App Store link of the app you want to downgrade.") + .font(.caption) + } + TextField("App share Link", text: $appLink) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding() + Button("Downgrade") { + if appLink.isEmpty { + return + } + var appLinkParsed = appLink + appLinkParsed = appLinkParsed.components(separatedBy: "id").last ?? "" + for char in appLinkParsed { + if !char.isNumber { + appLinkParsed = String(appLinkParsed.prefix(upTo: appLinkParsed.firstIndex(of: char)!)) + break + } + } + print("App ID: \(appLinkParsed)") + isDowngrading = true + downgradeApp(appId: appLinkParsed, ipaTool: ipaTool!) + } + .padding() + + Button("Log out and exit") { + isAuthenticated = false + EncryptedKeychainWrapper.nuke() + EncryptedKeychainWrapper.generateAndStoreKey() + sleep(3) + exit(0) // scuffed + } + .padding() + } + } + Spacer() + FooterView() } .padding() + .onAppear { + isAuthenticated = EncryptedKeychainWrapper.hasAuthInfo() + print("Found \(isAuthenticated ? "auth" : "no auth") info in keychain") + if isAuthenticated { + guard let authInfo = EncryptedKeychainWrapper.getAuthInfo() else { + print("Failed to get auth info from keychain, logging out") + isAuthenticated = false + EncryptedKeychainWrapper.nuke() + EncryptedKeychainWrapper.generateAndStoreKey() + return + } + appleId = authInfo["appleId"]! as! String + password = authInfo["password"]! as! String + ipaTool = IPATool(appleId: appleId, password: password) + let ret = ipaTool?.authenticate() + print("Re-authenticated \(ret! ? "successfully" : "unsuccessfully")") + } else { + print("No auth info found in keychain, setting up by generating a key in SEP") + EncryptedKeychainWrapper.generateAndStoreKey() + } + } } } diff --git a/MuffinStoreJailed/Downgrader.swift b/MuffinStoreJailed/Downgrader.swift new file mode 100644 index 0000000..9f86e19 --- /dev/null +++ b/MuffinStoreJailed/Downgrader.swift @@ -0,0 +1,131 @@ +// +// Downgrader.swift +// MuffinStoreJailed +// +// Created by Mineek on 19/10/2024. +// + +import Foundation +import UIKit +import Telegraph +import Zip + +func downgradeAppToVersion(appId: String, versionId: String, ipaTool: IPATool) { + let path = ipaTool.downloadIPAForVersion(appId: appId, appVerId: versionId) + print("IPA downloaded to \(path)") + + let tempDir = FileManager.default.temporaryDirectory + var contents = try! FileManager.default.contentsOfDirectory(atPath: path) + print("Contents: \(contents)") + let destinationUrl = tempDir.appendingPathComponent("app.ipa") + try! Zip.zipFiles(paths: contents.map { URL(fileURLWithPath: path).appendingPathComponent($0) }, zipFilePath: destinationUrl, password: nil, progress: nil) + print("IPA zipped to \(destinationUrl)") + let path2 = URL(fileURLWithPath: path) + var appDir = path2.appendingPathComponent("Payload") + for file in try! FileManager.default.contentsOfDirectory(atPath: appDir.path) { + if file.hasSuffix(".app") { + print("Found app: \(file)") + appDir = appDir.appendingPathComponent(file) + break + } + } + let infoPlistPath = appDir.appendingPathComponent("Info.plist") + let infoPlist = NSDictionary(contentsOf: infoPlistPath)! + let appBundleId = infoPlist["CFBundleIdentifier"] as! String + let appVersion = infoPlist["CFBundleShortVersionString"] as! String + print("appBundleId: \(appBundleId)") + print("appVersion: \(appVersion)") + + DispatchQueue.global(qos: .background).async { + let server = Server() + + server.route(.GET, "signed.ipa", { _ in + print("Serving signed.ipa") + let signedIPAData = try Data(contentsOf: destinationUrl) + return HTTPResponse(body: signedIPAData) + }) + + try! server.start(port: 9090) + print("Server has started listening") + + DispatchQueue.main.async { + print("Requesting app install") + let finalURL = "https://api.palera.in/genPlist?bundleid=\(appBundleId)&name=\(appBundleId)&version=\(appVersion)&fetchurl=http://127.0.0.1:9090/signed.ipa" + let finalURLfr = "itms-services://?action=download-manifest&url=" + finalURL.addingPercentEncoding(withAllowedCharacters: .alphanumerics)! + UIApplication.shared.open(URL(string: finalURLfr)!) + } + + while server.isRunning { + sleep(1) + } + print("Server has stopped") + } +} + +func promptForVersionId(appId: String, versionIds: [String], ipaTool: IPATool) { + let isiPad = UIDevice.current.userInterfaceIdiom == .pad + let alert = UIAlertController(title: "Enter version ID", message: "Select a version to downgrade to", preferredStyle: isiPad ? .alert : .actionSheet) + for versionId in versionIds { + alert.addAction(UIAlertAction(title: versionId, style: .default, handler: { _ in + downgradeAppToVersion(appId: appId, versionId: versionId, ipaTool: ipaTool) + })) + } + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true, completion: nil) +} + +func showAlert(title: String, message: String) { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true, completion: nil) +} + +func getAllAppVersionIdsFromServer(appId: String, ipaTool: IPATool) { + let serverURL = "https://apis.bilin.eu.org/history/" + let url = URL(string: "\(serverURL)\(appId)")! + let request = URLRequest(url: url) + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + DispatchQueue.main.async { + showAlert(title: "Error", message: error.localizedDescription) + } + return + } + let json = try! JSONSerialization.jsonObject(with: data!) as! [String: Any] + let versionIds = json["data"] as! [Dictionary] + if versionIds.count == 0 { + DispatchQueue.main.async { + showAlert(title: "Error", message: "No version IDs, internal error maybe?") + } + return + } + DispatchQueue.main.async { + let isiPad = UIDevice.current.userInterfaceIdiom == .pad + let alert = UIAlertController(title: "Select a version", message: "Select a version to downgrade to", preferredStyle: isiPad ? .alert : .actionSheet) + for versionId in versionIds { + alert.addAction(UIAlertAction(title: "\(versionId["bundle_version"]!)", style: .default, handler: { _ in + downgradeAppToVersion(appId: appId, versionId: "\(versionId["external_identifier"]!)", ipaTool: ipaTool) + })) + } + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true, completion: nil) + } + } + task.resume() +} + +func downgradeApp(appId: String, ipaTool: IPATool) { + let versionIds = ipaTool.getVersionIDList(appId: appId) + var selectedVersion = "" + let isiPad = UIDevice.current.userInterfaceIdiom == .pad + + let alert = UIAlertController(title: "Version ID", message: "Do you want to enter the version ID manually or request the list of version IDs from the server?", preferredStyle: isiPad ? .alert : .actionSheet) + alert.addAction(UIAlertAction(title: "Manual", style: .default, handler: { _ in + promptForVersionId(appId: appId, versionIds: versionIds, ipaTool: ipaTool) + })) + alert.addAction(UIAlertAction(title: "Server", style: .default, handler: { _ in + getAllAppVersionIdsFromServer(appId: appId, ipaTool: ipaTool) + })) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true, completion: nil) +} diff --git a/MuffinStoreJailed/IPATool.swift b/MuffinStoreJailed/IPATool.swift new file mode 100644 index 0000000..c72dd9d --- /dev/null +++ b/MuffinStoreJailed/IPATool.swift @@ -0,0 +1,510 @@ +// +// IPATool.swift +// MuffinStoreJailed +// +// Created by Mineek on 19/10/2024. +// + +// Heavily inspired by ipatool-py. +// https://github.com/NyaMisty/ipatool-py + +import Foundation +import CommonCrypto +import Zip + +extension Data { + var hexString: String { + return map { String(format: "%02x", $0) }.joined() + } +} + +class SHA1 { + static func hash(_ data: Data) -> Data { + var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH)) + data.withUnsafeBytes { + _ = CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest) + } + return Data(digest) + } +} + +extension String { + subscript (i: Int) -> String { + return String(self[index(startIndex, offsetBy: i)]) + } + + subscript (r: Range) -> String { + let start = index(startIndex, offsetBy: r.lowerBound) + let end = index(startIndex, offsetBy: r.upperBound) + return String(self[start.. String { + print("Generating GUID") + let DEFAULT_GUID = "000C2941396B" + let GUID_DEFAULT_PREFIX = 2 + let GUID_SEED = "CAFEBABE" + let GUID_POS = 10 + + let h = SHA1.hash((GUID_SEED + appleId + GUID_SEED).data(using: .utf8)!).hexString + let defaultPart = DEFAULT_GUID.prefix(GUID_DEFAULT_PREFIX) + let hashPart = h[GUID_POS.. Void { + var authCookiesEnc1 = NSKeyedArchiver.archivedData(withRootObject: authCookies!) + var authCookiesEnc = authCookiesEnc1.base64EncodedString() + var out: [String: Any] = [ + "appleId": appleId, + "password": password, + "guid": guid, + "accountName": accountName, + "authHeaders": authHeaders, + "authCookies": authCookiesEnc + ] + var data = try! JSONSerialization.data(withJSONObject: out, options: []) + var base64 = data.base64EncodedString() + EncryptedKeychainWrapper.saveAuthInfo(base64: base64) + } + + func tryLoadAuthInfo() -> Bool { + if let base64 = EncryptedKeychainWrapper.loadAuthInfo() { + var data = Data(base64Encoded: base64)! + var out = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any] + appleId = out["appleId"] as! String + password = out["password"] as! String + guid = out["guid"] as? String + accountName = out["accountName"] as? String + authHeaders = out["authHeaders"] as? [String: String] + var authCookiesEnc = out["authCookies"] as! String + var authCookiesEnc1 = Data(base64Encoded: authCookiesEnc)! + authCookies = NSKeyedUnarchiver.unarchiveObject(with: authCookiesEnc1) as? [HTTPCookie] + print("Loaded auth info") + return true + } + print("No auth info found, need to authenticate") + return false + } + + func authenticate() -> Bool { + if self.guid == nil { + self.guid = generateGuid(appleId: appleId) + } + + var req = [ + "appleId": appleId, + "password": password, + "guid": guid!, + "rmp": "0", + "why": "signIn" + ] + + var url = URL(string: "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/authenticate")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.allHTTPHeaderFields = [ + "Accept": "*/*", + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "Configurator/2.17 (Macintosh; OS X 15.2; 24C5089c) AppleWebKit/0620.1.16.11.6" + ] + + var ret = false + + for attempt in 1...4 { + req["attempt"] = String(attempt) + request.httpBody = try! JSONSerialization.data(withJSONObject: req, options: []) + let datatask = session.dataTask(with: request) { (data, response, error) in + if let error = error { + print("error 1 \(error.localizedDescription)") + return + } + if let response = response { +// print("Response: \(response)") + if let response = response as? HTTPURLResponse { + print("New URL: \(response.url!)") + request.url = response.url + } + } + if let data = data { + do { + let resp = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as! [String: Any] + if resp["m-allowed"] as! Bool { + print("Authentication successful") + var download_queue_info = resp["download-queue-info"] as! [String: Any] + var dsid = download_queue_info["dsid"] as! Int + var httpResp = response as! HTTPURLResponse + var storeFront = httpResp.value(forHTTPHeaderField: "x-set-apple-store-front") + print("Store front: \(storeFront!)") + self.authHeaders = [ + "X-Dsid": String(dsid), + "iCloud-Dsid": String(dsid), + "X-Apple-Store-Front": storeFront!, + "X-Token": resp["passwordToken"] as! String + ] + self.authCookies = self.session.configuration.httpCookieStorage?.cookies + var accountInfo = resp["accountInfo"] as! [String: Any] + var address = accountInfo["address"] as! [String: String] + self.accountName = address["firstName"]! + " " + address["lastName"]! + self.saveAuthInfo() + ret = true + } else { + print("Authentication failed: \(resp["customerMessage"] as! String)") + } + } catch { + print("Error: \(error)") + } + } + } + datatask.resume() + while datatask.state != .completed { + sleep(1) + } + if ret { + break + } + } + return ret + } + + func volumeStoreDownloadProduct(appId: String, appVerId: String = "") -> [String: Any] { + var req = [ + "creditDisplay": "", + "guid": self.guid!, + "salableAdamId": appId, + ] + if appVerId != "" { + req["externalVersionId"] = appVerId + } + var url = URL(string: "https://p25-buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/volumeStoreDownloadProduct?guid=\(self.guid!)")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.allHTTPHeaderFields = [ + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "Configurator/2.17 (Macintosh; OS X 15.2; 24C5089c) AppleWebKit/0620.1.16.11.6" + ] + request.httpBody = try! JSONSerialization.data(withJSONObject: req, options: []) + print("Setting headers") + for (key, value) in self.authHeaders! { + print("Setting header \(key): \(value)") + request.addValue(value, forHTTPHeaderField: key) + } + print("Setting cookies") + self.session.configuration.httpCookieStorage?.setCookies(self.authCookies!, for: url, mainDocumentURL: nil) + + var resp = [String: Any]() + let datatask = session.dataTask(with: request) { (data, response, error) in + if let error = error { + print("error 2 \(error.localizedDescription)") + return + } + if let data = data { + do { + print("Got response") + let resp1 = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as! [String: Any] + if resp1["cancel-purchase-batch"] != nil { + print("Failed to download product: \(resp1["customerMessage"] as! String)") + } + resp = resp1 + } catch { + print("Error: \(error)") + } + } + } + datatask.resume() + while datatask.state != .completed { + sleep(1) + } + print("Got download response") + return resp + } + + func download(appId: String, appVer: String = "", isRedownload: Bool = false) -> [String: Any] { + return self.volumeStoreDownloadProduct(appId: appId, appVerId: appVer) + } + + func downloadToPath(url: String, path: String) -> Void { + var req = URLRequest(url: URL(string: url)!) + req.httpMethod = "GET" + let datatask = session.dataTask(with: req) { (data, response, error) in + if let error = error { + print("error 3 \(error.localizedDescription)") + return + } + if let data = data { + do { + try data.write(to: URL(fileURLWithPath: path)) + } catch { + print("Error: \(error)") + } + } + } + datatask.resume() + while datatask.state != .completed { + sleep(1) + } + print("Downloaded to \(path)") + } +} + +class IPATool { + var session: URLSession + var appleId: String + var password: String + var storeClient: StoreClient + + init(appleId: String, password: String) { + print("init!") + session = URLSession.shared + self.appleId = appleId + self.password = password + storeClient = StoreClient(appleId: appleId, password: password) + } + + func authenticate() -> Bool { + print("Authenticating to iTunes Store...") + if !storeClient.tryLoadAuthInfo() { + // storeClient.authenticate() + return storeClient.authenticate() + } else { + return true + } + } + + func getVersionIDList(appId: String) -> [String] { + print("Retrieving download info for appId \(appId)") + var downResp = storeClient.download(appId: appId, isRedownload: true) + var songList = downResp["songList"] as! [[String: Any]] + if songList.count == 0 { + print("Failed to get app download info!") + return [] + } + var downInfo = songList[0] + var metadata = downInfo["metadata"] as! [String: Any] + var appVerIds = metadata["softwareVersionExternalIdentifiers"] as! [Int] + print("Got available version ids \(appVerIds)") + return appVerIds.map { String($0) } + } + + func downloadIPAForVersion(appId: String, appVerId: String) -> String { + print("Downloading IPA for app \(appId) version \(appVerId)") + var downResp = storeClient.download(appId: appId, appVer: appVerId) + var songList = downResp["songList"] as! [[String: Any]] + if songList.count == 0 { + print("Failed to get app download info!") + return "" + } + var downInfo = songList[0] + var url = downInfo["URL"] as! String + print("Got download URL: \(url)") + var fm = FileManager.default + var tempDir = fm.temporaryDirectory + var path = tempDir.appendingPathComponent("app.ipa").path + if fm.fileExists(atPath: path) { + print("Removing existing file at \(path)") + try! fm.removeItem(atPath: path) + } + storeClient.downloadToPath(url: url, path: path) + Zip.addCustomFileExtension("ipa") + sleep(3) + let path3 = URL(string: path)! + let fileExtension = path3.pathExtension + let fileName = path3.lastPathComponent + let directoryName = fileName.replacingOccurrences(of: ".\(fileExtension)", with: "") + let documentsUrl = fm.urls(for: .documentDirectory, in: .userDomainMask)[0] + let destinationUrl = documentsUrl.appendingPathComponent(directoryName, isDirectory: true) + if fm.fileExists(atPath: destinationUrl.path) { + print("Removing existing folder at \(destinationUrl.path)") + try! fm.removeItem(at: destinationUrl) + } + + let unzipDirectory = try! Zip.quickUnzipFile(URL(string: path)!) + var metadata = downInfo["metadata"] as! [String: Any] + var metadataPath = unzipDirectory.appendingPathComponent("iTunesMetadata.plist").path + metadata["apple-id"] = appleId + metadata["userName"] = appleId + try! (metadata as NSDictionary).write(toFile: metadataPath, atomically: true) + print("Wrote iTunesMetadata.plist") + var appContentDir = "" + let payloadDir = unzipDirectory.appendingPathComponent("Payload") + for entry in try! fm.contentsOfDirectory(atPath: payloadDir.path) { + if entry.hasSuffix(".app") { + print("Found app content dir: \(entry)") + appContentDir = "Payload/" + entry + break + } + } + print("Found app content dir: \(appContentDir)") + var scManifestData = try! Data(contentsOf: unzipDirectory.appendingPathComponent(appContentDir).appendingPathComponent("SC_Info").appendingPathComponent("Manifest.plist")) + var scManifest = try! PropertyListSerialization.propertyList(from: scManifestData, options: [], format: nil) as! [String: Any] + var sinfsDict = downInfo["sinfs"] as! [[String: Any]] + if let sinfPaths = scManifest["SinfPaths"] as? [String] { + for (i, sinfPath) in sinfPaths.enumerated() { + let sinfData = sinfsDict[i]["sinf"] as! Data + try! sinfData.write(to: unzipDirectory.appendingPathComponent(appContentDir).appendingPathComponent(sinfPath)) + print("Wrote sinf to \(sinfPath)") + } + } else { + print("Manifest.plist does not exist! Assuming it is an old app without one...") + var infoListData = try! Data(contentsOf: unzipDirectory.appendingPathComponent(appContentDir).appendingPathComponent("Info.plist")) + var infoList = try! PropertyListSerialization.propertyList(from: infoListData, options: [], format: nil) as! [String: Any] + var sinfPath = appContentDir + "/SC_Info/" + (infoList["CFBundleExecutable"] as! String) + ".sinf" + let sinfData = sinfsDict[0]["sinf"] as! Data + try! sinfData.write(to: unzipDirectory.appendingPathComponent(sinfPath)) + print("Wrote sinf to \(sinfPath)") + } + print("Downloaded IPA to \(unzipDirectory.path)") + return unzipDirectory.path + } +} + +class EncryptedKeychainWrapper { + static func generateAndStoreKey() -> Void { + self.deleteKey() + print("Generating key") + let query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, + kSecAttrKeySizeInBits as String: 256, + kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, + kSecPrivateKeyAttrs as String: [ + kSecAttrIsPermanent as String: true, + kSecAttrApplicationTag as String: "dev.mineek.muffinstorejailed.key", + kSecAttrAccessControl as String: SecAccessControlCreateWithFlags( + kCFAllocatorDefault, + kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + [.privateKeyUsage, .biometryAny], + nil + )! + ] + ] + var error: Unmanaged? + guard let privateKey = SecKeyCreateRandomKey(query as CFDictionary, &error) else { + print("Failed to generate key!!") + return + } + print("Generated key!") + print("Getting public key") + let pubKey = SecKeyCopyPublicKey(privateKey)! + print("Got public key") + let pubKeyData = SecKeyCopyExternalRepresentation(pubKey, &error)! as Data + let pubKeyBase64 = pubKeyData.base64EncodedString() + print("Public key: \(pubKeyBase64)") + } + + static func deleteKey() -> Void { + let query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrApplicationTag as String: "dev.mineek.muffinstorejailed.key" + ] + SecItemDelete(query as CFDictionary) + } + + static func saveAuthInfo(base64: String) -> Void { + let fm = FileManager.default + let query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrApplicationTag as String: "dev.mineek.muffinstorejailed.key", + kSecReturnRef as String: true + ] + var keyRef: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &keyRef) + if status != errSecSuccess { + print("Failed to get key!") + return + } + print("Got key!") + let key = keyRef as! SecKey + print("Getting public key") + let pubKey = SecKeyCopyPublicKey(key)! + print("Got public key") + print("Encrypting data") + var error: Unmanaged? + guard let encryptedData = SecKeyCreateEncryptedData(pubKey, .eciesEncryptionCofactorVariableIVX963SHA256AESGCM, base64.data(using: .utf8)! as CFData, &error) else { + print("Failed to encrypt data!") + return + } + print("Encrypted data") + let path = fm.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("authinfo").path + fm.createFile(atPath: path, contents: encryptedData as Data, attributes: nil) + print("Saved encrypted auth info") + } + + static func loadAuthInfo() -> String? { + let fm = FileManager.default + let path = fm.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("authinfo").path + if !fm.fileExists(atPath: path) { + return nil + } + let data = fm.contents(atPath: path)! + let query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrApplicationTag as String: "dev.mineek.muffinstorejailed.key", + kSecReturnRef as String: true + ] + var keyRef: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &keyRef) + if status != errSecSuccess { + print("Failed to get key!") + return nil + } + print("Got key!") + let key = keyRef as! SecKey + let privKey = key + print("Decrypting data") + var error: Unmanaged? + guard let decryptedData = SecKeyCreateDecryptedData(privKey, .eciesEncryptionCofactorVariableIVX963SHA256AESGCM, data as CFData, &error) else { + print("Failed to decrypt data!") + return nil + } + print("Decrypted data") + return String(data: decryptedData as Data, encoding: .utf8) + } + + static func deleteAuthInfo() -> Void { + let fm = FileManager.default + let path = fm.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("authinfo").path + try! fm.removeItem(atPath: path) + } + + static func hasAuthInfo() -> Bool { + return loadAuthInfo() != nil + } + + static func getAuthInfo() -> [String: Any]? { + if let base64 = loadAuthInfo() { + var data = Data(base64Encoded: base64)! + var out = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any] + return out + } + return nil + } + + static func nuke() -> Void { + deleteAuthInfo() + deleteKey() + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..7755bb9 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# MuffinStore Jailed + +Hacked together on-device App Store client. + +I am not responsible for any issues caused by the usage of this tool, it's experimental and I will not be held accountable if anything happens. Use at your own risk. Although nothing should happen, just putting this here just in case.