Add actual code.
This commit is contained in:
parent
6f377aa3d5
commit
244b4bab5d
8 changed files with 933 additions and 10 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
build/
|
||||
.DS_Store
|
||||
*.ipa
|
||||
24
Makefile
Normal file
24
Makefile
Normal 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
|
||||
|
|
@ -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 */;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
131
MuffinStoreJailed/Downgrader.swift
Normal file
131
MuffinStoreJailed/Downgrader.swift
Normal 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)
|
||||
}
|
||||
510
MuffinStoreJailed/IPATool.swift
Normal file
510
MuffinStoreJailed/IPATool.swift
Normal 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
5
README.md
Normal 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.
|
||||
Loading…
Reference in a new issue