Add actual code.

This commit is contained in:
Mineek 2024-12-31 15:16:15 +01:00
parent 6f377aa3d5
commit 244b4bab5d
8 changed files with 933 additions and 10 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
build/
.DS_Store
*.ipa

24
Makefile Normal file
View file

@ -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

View file

@ -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 */;
}

View file

@ -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
}

View file

@ -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()
}
}
}
}

View file

@ -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<String, Any>]
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)
}

View file

@ -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<Int>) -> String {
let start = index(startIndex, offsetBy: r.lowerBound)
let end = index(startIndex, offsetBy: r.upperBound)
return String(self[start..<end])
}
}
class StoreClient {
var session: URLSession
var appleId: String
var password: String
var guid: String?
var accountName: String?
var authHeaders: [String: String]?
var authCookies: [HTTPCookie]?
init(appleId: String, password: String) {
session = URLSession.shared
self.appleId = appleId
self.password = password
self.guid = nil
self.accountName = nil
self.authHeaders = nil
self.authCookies = nil
}
func generateGuid(appleId: String) -> 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..<GUID_POS + (DEFAULT_GUID.count - GUID_DEFAULT_PREFIX)]
let guid = (defaultPart + hashPart).uppercased()
print("Came up with GUID: \(guid)")
return guid
}
func saveAuthInfo() -> 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<CFError>?
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<CFError>?
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<CFError>?
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()
}
}

5
README.md Normal file
View file

@ -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.