diff --git a/.editorconfig b/.editorconfig index bb8ea53d..df9a904b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,7 @@ root = true [*] -indent_style = tab +indent_style = space indent_size = 4 end_of_line = lf charset = utf-8 @@ -10,4 +10,5 @@ insert_final_newline = true [*.{yml,yaml}] indent_style = space -indent_size = 2 \ No newline at end of file +indent_size = 2 + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9e2b8fd7..480a94db 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,28 +1,19 @@ -name: "Build" +name: "Build & Bundle" on: push: branches: - main - workflow_dispatch: - release: - types: [published] +env: + BINARY_NAME: plumeimpactor + BUNDLE_NAME: Impactor jobs: - build: - strategy: - fail-fast: false - matrix: - platform: - - ubuntu-22.04 - - windows-latest - - macos-latest - - runs-on: ${{ matrix.platform }} + build-linux: + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - - name: Cache Rust dependencies uses: actions/cache@v4 with: @@ -30,22 +21,206 @@ jobs: ~/.cargo/registry ~/.cargo/git target - key: ${{ matrix.platform }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo- + key: linux-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: linux-cargo- - name: Install Rust stable uses: dtolnay/rust-toolchain@stable + + - name: Install Linux dependencies + run: | + sudo apt-get update + sudo apt-get install libgtk-3-dev libpng-dev libjpeg-dev libgl1-mesa-dev libglu1-mesa-dev libxkbcommon-dev libexpat1-dev libtiff-dev + + - name: Build Dependencies + run: | + cargo install patch-crate + cargo fetch --locked || true + cargo patch-crate --force + cargo install cargo-bundle + mkdir -p build/out + + - name: Build & Bundle (Linux) + run: | + cargo bundle --bin ${{ env.BINARY_NAME }} --package ${{ env.BINARY_NAME }} --release --format appimage + cp -R target/release/bundle/appimage/*.AppImage build/out/ + + - name: Upload Bundles + uses: actions/upload-artifact@v4 with: - targets: ${{ (matrix.platform == 'macos-latest') && 'aarch64-apple-darwin' || '' }} + name: ${{ env.BINARY_NAME }}-linux + path: build/out/* + + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: windows-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: windows-cargo- + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable - name: Setup Windows dependencies - if: runner.os == 'Windows' run: choco install strawberryperl make --no-progress - - name: Setup MSVC environment - if: runner.os == 'Windows' + - name: Setup Windows MSVC environment uses: ilammy/msvc-dev-cmd@v1 - - name: Build - run: cargo build --bin plumeimpactor --release + - name: Build Dependencies + run: | + cargo install patch-crate + cargo fetch --locked || true + cargo patch-crate --force + mkdir -p build/out + + - name: Build & Bundle (Windows) + run: | + cargo build --bin ${{ env.BINARY_NAME }} --release + cp target/release/${{ env.BINARY_NAME }}.exe build/out + + - name: Upload Bundles + uses: actions/upload-artifact@v4 + with: + name: ${{ env.BINARY_NAME }}-windows + path: build/out/* + + build-macos-arm: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: macos-arm-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: macos-arm-cargo- + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-darwin + + - name: Build Dependencies + run: | + cargo install patch-crate + cargo fetch --locked || true + cargo patch-crate --force + mkdir -p build/out + + - name: Build (macOS ARM) + run: | + cargo build --bin ${{ env.BINARY_NAME }} --release + strip target/release/${{ env.BINARY_NAME }} + cp target/release/${{ env.BINARY_NAME }} build/out/${{ env.BINARY_NAME }}-arm + + - name: Upload ARM Slice + uses: actions/upload-artifact@v4 + with: + name: ${{ env.BINARY_NAME }}-macos-slice-arm + path: build/out/${{ env.BINARY_NAME }}-arm + + build-macos-intel: + runs-on: macos-15-intel + steps: + - uses: actions/checkout@v4 + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: macos-intel-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: macos-intel-cargo- + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Build Dependencies + run: | + cargo install patch-crate + cargo fetch --locked || true + cargo patch-crate --force + mkdir -p build/out + + - name: Build (macOS Intel) + run: | + cargo build --bin ${{ env.BINARY_NAME }} --release + strip target/release/${{ env.BINARY_NAME }} + cp target/release/${{ env.BINARY_NAME }} build/out/${{ env.BINARY_NAME }}-intel + + - name: Upload Intel Slice + uses: actions/upload-artifact@v4 + with: + name: ${{ env.BINARY_NAME }}-macos-slice-intel + path: build/out/${{ env.BINARY_NAME }}-intel + + build-macos-universal: + runs-on: macos-latest + needs: [build-macos-arm, build-macos-intel] + steps: + - uses: actions/checkout@v4 + + - name: Get ARM Slice + uses: actions/download-artifact@v4 + with: + name: ${{ env.BINARY_NAME }}-macos-slice-arm + path: build/slices + + - name: Get Intel Slice + uses: actions/download-artifact@v4 + with: + name: ${{ env.BINARY_NAME }}-macos-slice-intel + path: build/slices + + - name: Setup Certificates + uses: apple-actions/import-codesign-certs@v5 + with: + p12-file-base64: ${{ secrets.DEV_ID_P12_BASE64 }} + p12-password: ${{ secrets.DEV_ID_P12_PASSWORD }} + + - name: Create Universal Binary + run: | + mkdir -p build/universal + lipo -create -output build/universal/${{ env.BINARY_NAME }} build/slices/${{ env.BINARY_NAME }}-arm build/slices/${{ env.BINARY_NAME }}-intel + chmod +x build/universal/${{ env.BINARY_NAME }} + + - name: Bundle + run: | + mkdir -p build/dmg + cp -R package/macos/${{ env.BUNDLE_NAME }}.app build/dmg/${{ env.BUNDLE_NAME }}.app + mkdir -p build/dmg/${{ env.BUNDLE_NAME }}.app/Contents/MacOS + mv build/universal/${{ env.BINARY_NAME }} build/dmg/${{ env.BUNDLE_NAME }}.app/Contents/MacOS/${{ env.BINARY_NAME }} + + - name: Codesign + run: | + codesign --deep --force --options runtime \ + --sign "${{ secrets.DEV_ID_IDENTITY_NAME }}" build/dmg/${{ env.BUNDLE_NAME }}.app + + - name: Create DMG + run: | + mkdir -p build/out + ln -s /Applications build/dmg/Applications + hdiutil create -volname ${{ env.BUNDLE_NAME }} -srcfolder build/dmg -ov -format UDZO build/out/${{ env.BUNDLE_NAME }}.dmg + + - name: Notarize DMG + run: | + xcrun notarytool submit build/out/${{ env.BUNDLE_NAME }}.dmg --apple-id "${{ secrets.APPLE_ID_EMAIL }}" --password "${{ secrets.APPLE_ID_PASSWORD }}" --team-id "${{ secrets.APPLE_ID_TEAM }}" --wait + xcrun stapler staple build/out/${{ env.BUNDLE_NAME }}.dmg + + - name: Upload Universal DMG + uses: actions/upload-artifact@v4 + with: + name: ${{ env.BINARY_NAME }}-macos-universal + path: build/out/${{ env.BUNDLE_NAME }}.dmg diff --git a/.gitignore b/.gitignore index 84361743..2a709e9a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ state.plist .zsign_cache *.DS_Store /tests +/build diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..d0bb9c36 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "rust-lang.rust-analyzer", + "editorconfig.editorconfig" + ] +} diff --git a/Cargo.lock b/Cargo.lock index 9393537e..500d2ad5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,7 +181,7 @@ dependencies = [ "once_cell", "p12", "p256", - "pem", + "pem 3.0.6", "pkcs1", "pkcs8", "plist", @@ -190,7 +190,7 @@ dependencies = [ "rayon", "regex", "reqwest 0.12.23", - "ring", + "ring 0.17.14", "rsa", "scroll", "security-framework 2.11.1", @@ -237,17 +237,6 @@ dependencies = [ "thiserror 2.0.16", ] -[[package]] -name = "apple-native-keyring-store" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f9955235ce557bd0ea2c64d7ff09a887885f515e98572d2640a29520d9c98c" -dependencies = [ - "keyring-core", - "log", - "security-framework 3.5.1", -] - [[package]] name = "apple-xar" version = "0.20.0" @@ -458,6 +447,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -607,12 +602,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - [[package]] name = "bytes" version = "1.10.1" @@ -965,9 +954,9 @@ dependencies = [ "bytes", "chrono", "hex", - "pem", + "pem 3.0.6", "reqwest 0.12.23", - "ring", + "ring 0.17.14", "signature", "x509-certificate", ] @@ -1004,27 +993,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" -[[package]] -name = "dbus" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9" -dependencies = [ - "libc", - "libdbus-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "dbus-secret-service" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" -dependencies = [ - "dbus", - "zeroize", -] - [[package]] name = "der" version = "0.7.10" @@ -1255,37 +1223,12 @@ dependencies = [ "windows-sys 0.61.0", ] -[[package]] -name = "errors" -version = "0.0.1" -dependencies = [ - "apple-codesign", - "idevice", - "omnisette", - "pem", - "plist", - "reqwest 0.11.27", - "serde_json", - "thiserror 2.0.16", - "x509-certificate", - "zip 4.6.1", -] - [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - [[package]] name = "ff" version = "0.13.1" @@ -1336,9 +1279,9 @@ checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "libz-rs-sys", @@ -1561,23 +1504,32 @@ name = "grand_slam" version = "0.0.1" dependencies = [ "aes", + "apple-codesign", "base64 0.22.1", "botan", "cbc", - "errors", + "hex", "hmac", "omnisette", + "p12", "pbkdf2", + "pem 3.0.6", + "pem-rfc7468", "plist", - "rand 0.9.2", + "rand 0.8.5", + "rcgen", "reqwest 0.11.27", + "rsa", "rustls 0.23.32", "serde", "serde_json", + "sha1", "sha2", "srp", + "thiserror 2.0.16", "tokio", "uuid", + "x509-certificate", ] [[package]] @@ -2039,19 +1991,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "image" -version = "0.25.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" -dependencies = [ - "bytemuck", - "byteorder-lite", - "moxcms", - "num-traits", - "png", -] - [[package]] name = "indexmap" version = "2.11.4" @@ -2188,7 +2127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" dependencies = [ "byteorder", - "dbus-secret-service", + "linux-keyutils", "log", "security-framework 2.11.1", "security-framework 3.5.1", @@ -2196,15 +2135,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "keyring-core" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ad182c4841eb5795af9d20e6e020b65a895517f6a41e6358ed8af74ba35d98" -dependencies = [ - "log", -] - [[package]] name = "konst" version = "0.3.16" @@ -2231,19 +2161,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin", -] - -[[package]] -name = "ldid2" -version = "0.0.1" -dependencies = [ - "apple-codesign", - "errors", - "pem", - "plist", - "types", - "x509-certificate", + "spin 0.9.8", ] [[package]] @@ -2252,15 +2170,6 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" -[[package]] -name = "libdbus-sys" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" -dependencies = [ - "pkg-config", -] - [[package]] name = "libloading" version = "0.8.8" @@ -2297,6 +2206,16 @@ dependencies = [ "zlib-rs", ] +[[package]] +name = "linux-keyutils" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" +dependencies = [ + "bitflags 2.9.4", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -2437,16 +2356,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "moxcms" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08" -dependencies = [ - "num-traits", - "pxfm", -] - [[package]] name = "native-tls" version = "0.2.14" @@ -2662,15 +2571,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "openssl-src" -version = "300.5.3+3.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6bad8cd0233b63971e232cc9c5e83039375b8586d2312f31fda85db8f888c2" -dependencies = [ - "cc", -] - [[package]] name = "openssl-sys" version = "0.9.109" @@ -2679,7 +2579,6 @@ checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", - "openssl-src", "pkg-config", "vcpkg", ] @@ -2799,6 +2698,15 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + [[package]] name = "pem" version = "3.0.6" @@ -2906,18 +2814,18 @@ dependencies = [ name = "plumeimpactor" version = "0.0.1" dependencies = [ - "apple-native-keyring-store", "embed-manifest", "futures", "grand_slam", "idevice", - "image", "keyring", - "keyring-core", - "ldid2", + "plist", + "rustls 0.23.32", + "thiserror 2.0.16", "tokio", - "types", + "uuid", "wxdragon", + "zip 4.6.1", ] [[package]] @@ -2926,23 +2834,9 @@ version = "0.0.1" dependencies = [ "clap", "grand_slam", - "ldid2", - "openssl", "plist", - "types", -] - -[[package]] -name = "png" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" -dependencies = [ - "bitflags 2.9.4", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", + "rustls 0.23.32", + "tokio", ] [[package]] @@ -3025,15 +2919,6 @@ dependencies = [ "yansi", ] -[[package]] -name = "pxfm" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde" -dependencies = [ - "num-traits", -] - [[package]] name = "quick-xml" version = "0.38.3" @@ -3073,7 +2958,7 @@ dependencies = [ "getrandom 0.3.3", "lru-slab", "rand 0.9.2", - "ring", + "ring 0.17.14", "rustc-hash", "rustls 0.23.32", "rustls-pki-types", @@ -3217,6 +3102,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "rawzip" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70171b805bd97a69690df1b634104649ad193430aa60d8305dfb9b470fe690a8" + [[package]] name = "rayon" version = "1.11.0" @@ -3246,6 +3137,18 @@ dependencies = [ "cipher", ] +[[package]] +name = "rcgen" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6413f3de1edee53342e6138e75b56d32e7bc6e332b3bd62d497b1929d4cfbcdd" +dependencies = [ + "pem 1.1.1", + "ring 0.16.20", + "time", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.5.17" @@ -3410,6 +3313,21 @@ dependencies = [ "webpki-roots 1.0.3", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + [[package]] name = "ring" version = "0.17.14" @@ -3420,7 +3338,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -3494,7 +3412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", - "ring", + "ring 0.17.14", "rustls-webpki 0.101.7", "sct", ] @@ -3508,7 +3426,7 @@ dependencies = [ "aws-lc-rs", "log", "once_cell", - "ring", + "ring 0.17.14", "rustls-pki-types", "rustls-webpki 0.103.6", "subtle", @@ -3574,8 +3492,8 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring", - "untrusted", + "ring 0.17.14", + "untrusted 0.9.0", ] [[package]] @@ -3585,9 +3503,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" dependencies = [ "aws-lc-rs", - "ring", + "ring 0.17.14", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -3661,8 +3579,8 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring", - "untrusted", + "ring 0.17.14", + "untrusted 0.9.0", ] [[package]] @@ -3931,6 +3849,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spin" version = "0.9.8" @@ -4083,17 +4007,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "tar" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "tempfile" version = "3.22.0" @@ -4459,19 +4372,6 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" -[[package]] -name = "types" -version = "0.0.1" -dependencies = [ - "errors", - "idevice", - "plist", - "thiserror 2.0.16", - "tokio", - "uuid", - "zip 4.6.1", -] - [[package]] name = "typewit" version = "1.14.2" @@ -4514,6 +4414,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -5141,12 +5047,12 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "wxdragon" -version = "0.8.30" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a33d6cc76a0ac87ae24ea6595c344c13d3a704486757ca4d3713e08379c7a09" +checksum = "387582e9b06f1665c4807211e8a768911dfca14bd5fd4920456b4ceb96b82720" dependencies = [ "bitflags 2.9.4", - "lazy_static", + "log", "paste", "wxdragon-macros", "wxdragon-sys", @@ -5154,9 +5060,9 @@ dependencies = [ [[package]] name = "wxdragon-macros" -version = "0.8.30" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fb6c47a6840ead6fb9976fe67d31bd81eae4e9899f007ee43291a9267653f2" +checksum = "22c0e49dbb26c84bba760b56b858982f93e0effcb521e8a6b598ba082fb2616c" dependencies = [ "proc-macro2", "quick-xml", @@ -5166,15 +5072,17 @@ dependencies = [ [[package]] name = "wxdragon-sys" -version = "0.8.30" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b3de839d51d2be358782f4077902784ef2355f1e2bc81837a17b54761bdfe4a" +checksum = "39c289d767e8334f4d5792b887bad08283b1596240b257db2fae9301cdd925ae" dependencies = [ "bindgen", + "cmake", "flate2", "pkg-config", + "rawzip", "reqwest 0.12.23", - "tar", + "sha2", ] [[package]] @@ -5207,24 +5115,14 @@ dependencies = [ "chrono", "der", "hex", - "pem", - "ring", + "pem 3.0.6", + "ring 0.17.14", "signature", "spki", "thiserror 1.0.69", "zeroize", ] -[[package]] -name = "xattr" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" -dependencies = [ - "libc", - "rustix", -] - [[package]] name = "xmas-elf" version = "0.9.1" @@ -5260,6 +5158,9 @@ name = "yasna" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] [[package]] name = "yoke" diff --git a/Cargo.toml b/Cargo.toml index 7198bbd3..ae9fa63c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,20 +2,30 @@ resolver = "3" members = [ "apps/plumeimpactor", - "apps/plumesign", - "crates/errors", - "crates/ldid2", - "crates/grand_slam", - "crates/types" + "apps/plumesign", + "crates/grand_slam" ] [workspace.package] edition = "2024" version = "0.0.1" authors = ["khcrysalis "] -license = "GPL-3.0-or-later" +license = "MIT" repository = "https://github.com/khcrysalis/plumestore" +# We use `opt-level = "s"` as it significantly reduces binary size. +# We could then use the `#[optimize(speed)]` attribute for spot optimizations. +# Unfortunately, that attribute currently doesn't work on intrinsics such as memset. +[profile.release] +codegen-units = 1 # reduces binary size by ~2% +debug = "full" # No one needs an undebuggable release binary +lto = true # reduces binary size by ~14% +opt-level = "s" # reduces binary size by ~25% +panic = "abort" # reduces binary size by ~50% in combination with -Zbuild-std-features=panic_immediate_abort +split-debuginfo = "packed" # generates a separate *.dwp/*.dSYM so the binary can get stripped +strip = "symbols" # See split-debuginfo - allows us to drop the size by ~65% +incremental = true # Improves re-compile times + [workspace.dependencies] tokio = { version = "1.43", features = ["macros", "rt-multi-thread"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..1392705a --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright 2025 Samara M + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index ff8fb091..85b44ba4 100644 --- a/README.md +++ b/README.md @@ -1 +1,33 @@ # PlumeImpactor + +[![GitHub Release](https://img.shields.io/github/v/release/khcrysalis/PlumeImpactor?include_prereleases)](https://github.com/khcrysalis/PlumeImpactor/releases) +[![GitHub License](https://img.shields.io/github/license/khcrysalis/PlumeImpactor?color=%23C96FAD)](https://github.com/khcrysalis/PlumeImpactor/blob/main/LICENSE) +[![Sponsor Me](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/khcrysalis) + +PlumeImpactor is an open-source, cross-platform, and feature rich iOS/tvOS sideloading application. Supporting macOS, Linux, and Windows. + +### Features +- User friendly and clean UI. +- Sign and install applications. + +## Structure + +The project is seperated in multiple modules, all serve single or multiple uses depending on their importance. + +| Module | Description | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `apps/plumeimpactor` | GUI interface for the crates shown below, backend using wxWidgets (with a rust ffi wrapper, wxDragon) | +| `apps/plumesign` | CLI interface for the crates shown below, using `clap`. | +| `crates/grand_slam` | Handles all api request used for communicating with Apple developer services, along with providing auth for Apple's grandslam | + +## Acknowledgements + +- [SAMSAM](https://github.com/khcrysalis) – The maker. +- [SideStore](https://github.com/SideStore/apple-private-apis) – Grandslam auth & Omnisette. +- [Sideloader](https://github.com/Dadoum/Sideloader) – Apple Developer API references. +- [idevice](https://github.com/jkcoxson/idevice) – Used for communication with `installd`, specifically for sideloading the apps to your devices. +- [apple-codesign-rs](https://github.com/indygreg/apple-platform-rs) – Open-source alternative to codesign. + +## License + +Project is licensed under the MIT license. You can see the full details of the license [here](https://github.com/khcrysalis/PlumeImpactor/blob/main/LICENSE). diff --git a/apps/plumeimpactor/Cargo.toml b/apps/plumeimpactor/Cargo.toml index 984ab387..97317334 100644 --- a/apps/plumeimpactor/Cargo.toml +++ b/apps/plumeimpactor/Cargo.toml @@ -9,28 +9,18 @@ repository.workspace = true [dependencies] idevice.workspace = true +zip.workspace = true tokio.workspace = true -ldid2 = { path = "../../crates/ldid2" } +thiserror.workspace = true +uuid.workspace = true +plist.workspace = true grand_slam = { path = "../../crates/grand_slam", features = ["vendored-botan"] } -types = { path = "../../crates/types"} -wxdragon = "0.8.30" -image = { version = "0.25.1", default-features = false, features = ["png"] } -futures = "0.3.31" - -# Keyrings -[target.'cfg(all(debug_assertions, target_os = "macos"))'.dependencies] -keyring = { version = "3.6.3", features = ["apple-native"] } -[target.'cfg(all(not(debug_assertions), target_os = "macos"))'.dependencies] -keyring-core = { version = "0.7.0" } -apple-native-keyring-store = { version = "0.2.1", features = ["protected"] } -# keyring-core requires notarization with entitlements to work properly on macOS (release) - -[target.'cfg(target_os = "windows")'.dependencies] -keyring = { version = "3.6.3", features = ["windows-native"] } +wxdragon = "0.9.2" +futures = "0.3.31" +rustls = { version = "0.23.32", features = ["ring"] } -[target.'cfg(target_os = "linux")'.dependencies] -keyring = { version = "3.6.3", features = ["sync-secret-service"] } +keyring = { version = "3.6.3", default-features = false, features = ["windows-native", "apple-native", "linux-native"] } -[build-dependencies] +[target.'cfg(target_os = "windows")'.build-dependencies] embed-manifest = "1.4" diff --git a/apps/plumeimpactor/build.rs b/apps/plumeimpactor/build.rs index d6aa5aae..d4319d03 100644 --- a/apps/plumeimpactor/build.rs +++ b/apps/plumeimpactor/build.rs @@ -1,6 +1,4 @@ -use embed_manifest::manifest::{ActiveCodePage, Setting, SupportedOS::*}; -use embed_manifest::{embed_manifest, new_manifest}; - +#[cfg(windows)] fn main() { println!("cargo:rerun-if-changed=build.rs"); let target = std::env::var("TARGET").unwrap_or_default(); @@ -11,7 +9,14 @@ fn main() { } } +#[cfg(not(windows))] +fn main() {} + +#[cfg(windows)] fn embed_windows_manifest(name: &str) { + use embed_manifest::manifest::{ActiveCodePage, Setting, SupportedOS::*}; + use embed_manifest::{embed_manifest, new_manifest}; + let manifest = new_manifest(name) .supported_os(Windows7..=Windows10) .active_code_page(ActiveCodePage::Utf8) diff --git a/apps/plumeimpactor/resources/install.png b/apps/plumeimpactor/resources/install.png deleted file mode 100644 index 2915b1ff..00000000 Binary files a/apps/plumeimpactor/resources/install.png and /dev/null differ diff --git a/apps/plumeimpactor/src/frame.rs b/apps/plumeimpactor/src/frame.rs index 939337f0..b08f09ac 100644 --- a/apps/plumeimpactor/src/frame.rs +++ b/apps/plumeimpactor/src/frame.rs @@ -1,78 +1,78 @@ use std::cell::RefCell; use std::path::PathBuf; use std::rc::Rc; -use std::{env, ptr, thread}; +use std::{ptr, thread}; -use grand_slam::AnisetteConfiguration; +use grand_slam::{AnisetteConfiguration, BundleType, CertificateIdentity, MachO, MobileProvision, Signer}; use grand_slam::auth::Account; use grand_slam::developer::DeveloperSession; +use grand_slam::utils::{PlistInfoTrait}; +use idevice::utils::installation; use wxdragon::prelude::*; use futures::StreamExt; -use idevice::usbmuxd::{UsbmuxdConnection, UsbmuxdListenEvent}; +use idevice::usbmuxd::{UsbmuxdAddr, UsbmuxdConnection, UsbmuxdListenEvent}; use tokio::runtime::Builder; use tokio::sync::mpsc; -use types::{Device, Package}; -use crate::pages::login::{LoginDialog, create_single_field_dialog}; -use crate::pages::{DefaultPage, InstallPage, create_default_page, create_install_page, create_login_dialog}; -use crate::APP_NAME; +use crate::get_data_path; use crate::handlers::{PlumeFrameMessage, PlumeFrameMessageHandler}; +use crate::keychain::AccountCredentials; +use crate::pages::{LoginDialog, DefaultPage, InstallPage, SettingsDialog, WINDOW_SIZE, create_default_page, create_install_page, create_login_dialog, create_settings_dialog}; +use crate::utils::{Device, Package}; + +pub const APP_NAME: &str = concat!(env!("CARGO_PKG_NAME"), " – Version ", env!("CARGO_PKG_VERSION")); pub struct PlumeFrame { pub frame: Frame, pub default_page: DefaultPage, pub install_page: InstallPage, - pub settings_button: Button, pub usbmuxd_picker: Choice, - - pub apple_id_picker: Choice, + + pub add_ipa_button: Button, pub apple_id_button: Button, + pub login_dialog: LoginDialog, + pub settings_dialog: SettingsDialog, } impl PlumeFrame { pub fn new() -> Self { let frame = Frame::builder() .with_title(APP_NAME) - .with_size(Size::new(530, 410)) - .with_style(FrameStyle::CloseBox | FrameStyle::MinimizeBox) + .with_size(Size::new(WINDOW_SIZE.0, WINDOW_SIZE.1)) + .with_style(FrameStyle::CloseBox | FrameStyle::MinimizeBox | FrameStyle::Caption | FrameStyle::SystemMenu) .build(); - + let sizer = BoxSizer::builder(Orientation::Vertical).build(); - let top_panel = Panel::builder(&frame).build(); let top_row = BoxSizer::builder(Orientation::Horizontal).build(); - let device_picker = Choice::builder(&top_panel) - .build(); - - let apple_id_picker = Choice::builder(&top_panel) - .build(); - - let apple_id_button = Button::builder(&top_panel) - .with_label("+") - .build(); - - let settings_button = Button::builder(&top_panel) - .with_label("Settings") - .build(); + let add_ipa_button = Button::builder(&frame).with_label("Import").build(); + let device_picker = Choice::builder(&frame).build(); + let apple_id_button = Button::builder(&frame).with_label("Settings").build(); + top_row.add(&add_ipa_button, 0, SizerFlag::All, 0); + top_row.add_spacer(13); top_row.add(&device_picker, 1, SizerFlag::Expand | SizerFlag::All, 0); - top_row.add_spacer(8); - top_row.add(&apple_id_picker, 1, SizerFlag::Expand | SizerFlag::All, 0); - top_row.add_spacer(8); + top_row.add_spacer(13); top_row.add(&apple_id_button, 0, SizerFlag::All, 0); - top_row.add_spacer(8); - top_row.add(&settings_button, 0, SizerFlag::All, 0); - - top_panel.set_sizer(top_row, true); let default_page = create_default_page(&frame); let install_page = create_install_page(&frame); - sizer.add(&top_panel, 0, SizerFlag::Expand | SizerFlag::All, 8); - sizer.add(&default_page.panel, 1, SizerFlag::Expand | SizerFlag::All, 0); - sizer.add(&install_page.panel, 1, SizerFlag::Expand | SizerFlag::All, 0); + sizer.add_sizer(&top_row, 0, SizerFlag::Expand | SizerFlag::All, 13); + sizer.add( + &default_page.panel, + 1, + SizerFlag::Expand | SizerFlag::All, + 0, + ); + sizer.add( + &install_page.panel, + 1, + SizerFlag::Expand | SizerFlag::All, + 0, + ); frame.set_sizer(sizer, true); install_page.panel.hide(); @@ -80,15 +80,15 @@ impl PlumeFrame { frame: frame.clone(), default_page, install_page, - settings_button, usbmuxd_picker: device_picker, - apple_id_picker, + add_ipa_button, apple_id_button, login_dialog: create_login_dialog(&frame), + settings_dialog: create_settings_dialog(&frame), }; s.setup_event_handlers(); - + s } @@ -97,17 +97,27 @@ impl PlumeFrame { self.frame.centre(); self.frame.set_extra_style(ExtraWindowStyle::ProcessIdle); } +} +// MARK: - Event Handlers + +impl PlumeFrame { fn setup_event_handlers(&mut self) { let (sender, receiver) = mpsc::unbounded_channel::(); - - let message_handler = Rc::new( - RefCell::new(PlumeFrameMessageHandler::new( - receiver, - unsafe { ptr::read(self) }, - )) - ); - + let message_handler = self.setup_idle_handler(receiver); + Self::spawn_background_threads(sender.clone()); + self.bind_widget_handlers(sender, message_handler); + } + + fn setup_idle_handler( + &self, + receiver: mpsc::UnboundedReceiver, + ) -> Rc> { + let message_handler = Rc::new(RefCell::new(PlumeFrameMessageHandler::new( + receiver, + unsafe { ptr::read(self) }, + ))); + let handler_for_idle = message_handler.clone(); self.frame.on_idle(move |event_data| { if let WindowEventData::Idle(event) = event_data { @@ -115,186 +125,533 @@ impl PlumeFrame { } }); - // --- Usbmuxd Listener --- - - thread::spawn({ - let sender = sender.clone(); - move || { - let rt = Builder::new_current_thread().enable_io().build().unwrap(); + message_handler + } - rt.block_on(async move { - let mut muxer = match UsbmuxdConnection::default().await { - Ok(muxer) => muxer, - Err(e) => { - sender.send(PlumeFrameMessage::Error(format!("Failed to connect to usbmuxd: {}", e))).ok(); - return; - } - }; + fn spawn_background_threads(sender: mpsc::UnboundedSender) { + Self::spawn_usbmuxd_listener(sender.clone()); + Self::spawn_auto_login_thread(sender); + } - match muxer.get_devices().await { - Ok(devices) => { - for dev in devices { - sender.send(PlumeFrameMessage::DeviceConnected(Device::new(dev).await)).ok(); - } - } - Err(e) => { - sender.send(PlumeFrameMessage::Error(format!("Failed to get initial device list: {}", e))).ok(); + fn spawn_usbmuxd_listener(sender: mpsc::UnboundedSender) { + thread::spawn(move || { + let rt = Builder::new_current_thread().enable_io().build().unwrap(); + rt.block_on(async move { + let mut muxer = match UsbmuxdConnection::default().await { + Ok(muxer) => muxer, + Err(e) => { + sender.send(PlumeFrameMessage::Error(format!("Failed to connect to usbmuxd: {}", e))).ok(); + return; + } + }; + + match muxer.get_devices().await { + Ok(devices) => { + for dev in devices { + sender.send(PlumeFrameMessage::DeviceConnected(Device::new(dev).await)).ok(); } } + Err(e) => { + sender.send(PlumeFrameMessage::Error(format!("Failed to get initial device list: {}", e))).ok(); + } + } + + let mut stream = match muxer.listen().await { + Ok(stream) => stream, + Err(e) => { + sender.send(PlumeFrameMessage::Error(format!("Failed to listen for events: {}", e))).ok(); + return; + } + }; - let mut stream = match muxer.listen().await { - Ok(stream) => stream, + while let Some(event) = stream.next().await { + let msg = match event { + Ok(dev_event) => match dev_event { + UsbmuxdListenEvent::Connected(dev) => { + PlumeFrameMessage::DeviceConnected(Device::new(dev).await) + } + UsbmuxdListenEvent::Disconnected(device_id) => { + PlumeFrameMessage::DeviceDisconnected(device_id) + } + }, Err(e) => { - sender.send(PlumeFrameMessage::Error(format!("Failed to listen for events: {}", e))).ok(); - return; + PlumeFrameMessage::Error(format!("Failed to listen for events: {}", e)) } }; - while let Some(event) = stream.next().await { - let msg = match event { - Ok(dev_event) => match dev_event { - UsbmuxdListenEvent::Connected(dev) => { - PlumeFrameMessage::DeviceConnected(Device::new(dev).await) - } - UsbmuxdListenEvent::Disconnected(device_id) => { - PlumeFrameMessage::DeviceDisconnected(device_id) - } - }, - Err(e) => { - PlumeFrameMessage::Error(format!("Failed to listen for events: {}", e)) - } - }; - if sender.send(msg).is_err() { - break; - } + if sender.send(msg).is_err() { + break; } - }); - } + } + }); }); + } + + fn spawn_auto_login_thread(sender: mpsc::UnboundedSender) { + thread::spawn(move || { + let creds = AccountCredentials; - // --- GUI Handlers --- + let (email, password) = match (creds.get_email(), creds.get_password()) { + (Ok(email), Ok(password)) => (email, password), + _ => { return; } + }; - let handler_for_choice = message_handler.clone(); - let picker_clone = self.usbmuxd_picker.clone(); - self.usbmuxd_picker.on_selection_changed(move |_event_data| { - let mut handler = handler_for_choice.borrow_mut(); - - if let Some(index) = picker_clone.get_selection() { - if let Some(selected_item) = handler.usbmuxd_device_list.get(index as usize) { - handler.usbmuxd_selected_device_id = Some(selected_item.usbmuxd_device.device_id.to_string()); + match run_login_flow(sender.clone(), &email, &password) { + Ok(account) => { + sender.send(PlumeFrameMessage::AccountLogin(account)).ok(); + } + Err(e) => { + sender.send(PlumeFrameMessage::AccountDeleted).ok(); + sender.send(PlumeFrameMessage::Error(format!("Login error: {}", e))).ok(); } - } else { - handler.usbmuxd_selected_device_id = None; } }); - - self.settings_button.on_click(|_| { - println!("Settings"); + } +} + +// MARK: - Button Handlers + +impl PlumeFrame { + fn bind_widget_handlers( + &mut self, + sender: mpsc::UnboundedSender, + message_handler: Rc>, + ) { + // MARK: Device Picker + + self.usbmuxd_picker.on_selection_changed( { + let message_handler = message_handler.clone(); + let picker_clone = self.usbmuxd_picker.clone(); + move |_| { + let mut handler = message_handler.borrow_mut(); + handler.usbmuxd_selected_device_id = picker_clone + .get_selection() + .and_then(|i| handler.usbmuxd_device_list.get(i as usize)) + .map(|item| item.usbmuxd_device.device_id.to_string()); + } }); - let login_dialog_rc = Rc::new(self.login_dialog.clone()); + // MARK: Apple ID / Login Dialog + self.apple_id_button.on_click({ - let login_dialog = login_dialog_rc.clone(); + let account_dialog = Rc::new(self.settings_dialog.clone()); move |_| { - login_dialog.show_modal(); + account_dialog.dialog.show(true); } }); - self.login_dialog.set_cancel_handler({ - let login_dialog = login_dialog_rc.clone(); + self.settings_dialog.set_logout_handler({ + let message_handler = message_handler.clone(); + let sender = sender.clone(); + let login_dialog = self.login_dialog.clone(); move || { - login_dialog.clear_fields(); - login_dialog.hide(); + if message_handler.borrow().account_credentials.is_some() { + sender.send(PlumeFrameMessage::AccountDeleted).ok(); + } else { + login_dialog.dialog.show(true); + } + } + }); + + // MARK: File Drop/Open Handlers + + fn process_package_file(sender: mpsc::UnboundedSender, file_path: PathBuf) { + match Package::new(file_path) { + Ok(package) => { + sender.send(PlumeFrameMessage::PackageSelected(package)).ok(); + } + Err(e) => { + sender.send(PlumeFrameMessage::Error(format!("Failed to open package: {}", e))).ok(); + } + } + } + + #[cfg(not(target_os = "linux"))] + self.default_page.set_file_handlers({ + let sender = sender.clone(); + move |file_path| process_package_file(sender.clone(), PathBuf::from(file_path)) + }); + + self.add_ipa_button.on_click({ + let sender = sender.clone(); + let handler_for_import = self.frame.clone(); + move |_| { + let dialog = FileDialog::builder(&handler_for_import) + .with_message("Open IPA File") + .with_style(FileDialogStyle::default() | FileDialogStyle::Open) + .with_wildcard("IPA files (*.ipa;*.tipa)|*.ipa;*.tipa") + .build(); + + if dialog.show_modal() != ID_OK { + return; + } + + if let Some(file_path) = dialog.get_path() { + process_package_file(sender.clone(), PathBuf::from(file_path)); + } + } + }); + + // MARK: Install Page Handlers + + self.install_page.set_cancel_handler({ + let sender = sender.clone(); + move || { + sender.send(PlumeFrameMessage::PackageDeselected).ok(); } }); - let frame_clone = self.frame.clone(); - self.login_dialog.set_next_handler({ - let login_dialog = login_dialog_rc.clone(); + self.install_page.set_install_handler({ + let frame = self.frame.clone(); + let message_handler = message_handler.clone(); + let sender = sender.clone(); move || { - let email = login_dialog.get_email(); - let password = login_dialog.get_password(); + let binding = message_handler.borrow(); - login_dialog.clear_fields(); - login_dialog.hide(); + let Some(selected_device) = binding.usbmuxd_selected_device_id.as_deref() else { + sender.send(PlumeFrameMessage::Error("No device selected for installation.".to_string())).ok(); + return; + }; + + let Some(selected_package) = binding.package_selected.as_ref() else { + sender.send(PlumeFrameMessage::Error("No package selected for installation.".to_string())).ok(); + return; + }; + let Some(selected_account) = binding.account_credentials.as_ref() else { + sender.send(PlumeFrameMessage::Error("No Apple ID account available for installation.".to_string())).ok(); + return; + }; + + let mut signer_settings = binding.signer_settings.clone(); + binding.plume_frame.install_page.update_fields(&mut signer_settings); + + let package = selected_package.clone(); + let account = selected_account.clone(); + let device_id = selected_device.to_string(); + let sender_clone = sender.clone(); + + thread::spawn(move || { + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + let install_result = rt.block_on(async { + let session = DeveloperSession::with(account.clone()); + + sender_clone.send(PlumeFrameMessage::InstallProgress(10, Some("Ensuring current device is registered...".to_string()))).ok(); + + let mut usbmuxd = UsbmuxdConnection::default() + .await + .map_err(|e| format!("usbmuxd connect error: {e}"))?; + let usbmuxd_device = usbmuxd.get_devices() + .await + .map_err(|e| format!("usbmuxd device list error: {e}"))? + .into_iter() + .find(|d| d.device_id.to_string() == device_id) + .ok_or_else(|| format!("Device ID {device_id} not found"))?; + + let device = Device::new(usbmuxd_device.clone()).await; + + // TODO: Handle multiple teams properly + let teams = session.qh_list_teams() + .await + .map_err(|e| format!("Failed to list teams: {}", e))?.teams; + + if teams.len() != 1 { + return Err("Multiple teams detected for the Apple ID account.".to_string()); + } + + let team_id = &teams.get(0) + .ok_or("No teams available for the Apple ID account.")? + .team_id; + + let cert_identity: CertificateIdentity = if signer_settings.export_ipa { + CertificateIdentity { cert: None, key: None, machine_id: None, p12_data: None, serial_number: None } + } else { + let cert_identity = CertificateIdentity::new_with_session( + &session, + get_data_path(), + None, + team_id, + ).await.map_err(|e| e.to_string())?; + + cert_identity + }; - println!("Email: {}, Password: {}", email, password); + session.qh_ensure_device( + team_id, + &device.name, + &device.uuid, + ) + .await + .map_err(|e| format!("Failed to ensure device is registered: {}", e))?; + + sender_clone.send(PlumeFrameMessage::InstallProgress(20, Some("Extracting package...".to_string()))).ok(); + + let bundle = package.get_package_bundle() + .map_err(|e| format!("Failed to get package bundle: {}", e))?; + let bundles = bundle.collect_bundles_sorted() + .map_err(|e| format!("Failed to collect bundles: {}", e))?; + + let bundle_identifier = bundle.get_bundle_identifier() + .ok_or("Failed to get bundle identifier from package.")?; + + if let Some(new_name) = signer_settings.custom_name.as_ref() { + bundle.set_name(new_name).map_err(|e| format!("Failed to set new name: {}", e))?; + } - let anisette_config = AnisetteConfiguration::default() - .set_configuration_path(env::temp_dir()); + if let Some(new_version) = signer_settings.custom_version.as_ref() { + bundle.set_version(new_version).map_err(|e| format!("Failed to set new version: {}", e))?; + } + + if signer_settings.support_minimum_os_version { + bundle.set_info_plist_key("MinimumOSVersion", "7.0").map_err(|e| format!("Failed to set minimum OS version: {}", e))?; + } + + if signer_settings.support_file_sharing { + bundle.set_info_plist_key("UIFileSharingEnabled", true).map_err(|e| format!("Failed to set file sharing: {}", e))?; + bundle.set_info_plist_key("UISupportsDocumentBrowser", true).map_err(|e| format!("Failed to set document opening: {}", e))?; + } + + if signer_settings.support_ipad_fullscreen { + bundle.set_info_plist_key("UIRequiresFullScreen", true).map_err(|e| format!("Failed to set iPad fullscreen: {}", e))?; + } - thread::spawn({ - let email = email.clone(); - let password = password.clone(); - let anisette_config = anisette_config; - move || { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - rt.block_on(async { + if signer_settings.support_game_mode { + bundle.set_info_plist_key("GCSupportsGameMode", true).map_err(|e| format!("Failed to set game mode: {}", e))?; + } - let get_2fa_code = || { + if signer_settings.support_pro_motion { + bundle.set_info_plist_key("CADisableMinimumFrameDurationOnPhone", true).map_err(|e| format!("Failed to set document opening: {}", e))?; + } - Err("2FA code request not implemented in background thread".into()) - }; + if !signer_settings.export_ipa { + if signer_settings.custom_identifier.is_none() { + signer_settings.custom_identifier = Some(format!("{bundle_identifier}.{team_id}")); + } + } + + if let Some(new_identifier) = signer_settings.custom_identifier.as_ref() { + for embedded_bundle in &bundles { + embedded_bundle.set_matching_identifier( + &bundle_identifier, + &new_identifier, + ).map_err(|e| format!("Failed to set matching identifier: {}", e))?; + } + } + + // if signer_settings.should_embed_p12 { + // if let Some(p12_data) = &cert_identity.p12_data { + // if let Some(serial_number) = &cert_identity.serial_number { + // bundle.set_info_plist_key("ALTCertificateID", &**serial_number) + // .map_err(|e| format!("Failed to set cert serial: {}", e))?; + // fs::write(bundle.dir().join("ALTCertificate.p12"), p12_data) + // .map_err(|e| format!("Failed to write p12: {}", e))?; + // } + // } + // } + + sender_clone.send(PlumeFrameMessage::InstallProgress(30, Some(format!("Registering {}...", bundle.get_name().unwrap_or_default())))).ok(); + + let mut provisionings: Vec = Vec::new(); + + if !signer_settings.export_ipa { + for sub_bundle in &bundles { + if signer_settings.should_only_use_main_provisioning && sub_bundle.dir() != bundle.dir() { + continue; + } + + if + sub_bundle._type != BundleType::AppExtension && + sub_bundle._type != BundleType::App + { + continue; + } - let a = Account::login( - || Ok((email.clone(), password.clone())), - get_2fa_code, - anisette_config, - ).await.unwrap(); + let bundle_executable_name = sub_bundle.get_executable() + .ok_or("Failed to get executable from bundle.")?; + + let bundle_executable_path = sub_bundle.dir().join(&bundle_executable_name); + + let macho = MachO::new(&bundle_executable_path) + .map_err(|e| format!("Failed to read Mach-O binary: {}", e))?; + + let id = sub_bundle.get_bundle_identifier() + .ok_or("Failed to get bundle identifier from bundle.")?; + + println!("{}", id); + + session.qh_ensure_app_id(team_id, &sub_bundle.get_name().unwrap_or_default(), &id) + .await + .map_err(|e| format!("Failed to ensure app ID: {}", e))?; + + let capabilities = session.v1_list_capabilities(team_id) + .await + .map_err(|e| format!("Failed to list capabilities: {}", e))?; + + let app_id_id = session.qh_get_app_id(team_id, &id) + .await + .map_err(|e| e.to_string())? + .ok_or("Failed to get ensured app ID.")?; + + if let Some(caps) = macho.capabilities_for_entitlements(&capabilities.data) { + session.v1_update_app_id(team_id, &id, caps) + .await + .map_err(|e| format!("Failed to enable capabilities: {}", e))?; + } + + if let Some(app_groups) = macho.app_groups_for_entitlements() { + for group in &app_groups { + let group = format!("{group}.{team_id}"); + let group_id = session.qh_ensure_app_group(team_id, &group, &group) + .await + .map_err(|e| format!("Failed to ensure app group: {}", e))?; + + session.qh_assign_app_group(team_id, &app_id_id.app_id_id, &group_id.application_group) + .await + .map_err(|e| format!("Failed to add app group to app ID: {}", e))?; + } + } + + let profiles = session.qh_get_profile(team_id, &app_id_id.app_id_id) + .await + .map_err(|e| format!("Failed to list profiles: {}", e))?; + + let profile_data = profiles.provisioning_profile.encoded_profile; + + let mobile_provision = MobileProvision::load_with_bytes(profile_data.as_ref().to_vec()) + .map_err(|e| format!("Failed to load mobile provision: {}", e))?; + + provisionings.push(mobile_provision); + } + } + + sender_clone.send(PlumeFrameMessage::InstallProgress(50, Some(format!("Signing {}...", bundle.get_name().unwrap_or_default())))).ok(); + + let signer = Signer::new( + Some(cert_identity), + signer_settings.clone(), + provisionings, + ); + + signer.sign_bundle(&bundle) + .map_err(|e| format!("Failed to sign bundle: {}", e))?; + + if !signer_settings.export_ipa { + let provider = usbmuxd_device.to_provider(UsbmuxdAddr::from_env_var().unwrap(), "baller"); + + let bundle_name = bundle.get_name().unwrap_or_default(); + let callback = { + let sender_clone = sender_clone.clone(); + move |(progress, _): (u64, ())| { + let sender = sender_clone.clone(); + let bundle_name = bundle_name.clone(); + async move { + sender.send(PlumeFrameMessage::InstallProgress(progress as i32, Some(format!("Installing {}... {}%", bundle_name, progress)))).ok(); + } + } + }; + + let state = (); - let session = DeveloperSession::with(a); - let a = session.qh_list_teams().await.unwrap(); - println!("{:#?}", env::temp_dir()); - println!("{:#?}", a); + installation::install_package_with_callback(&provider, bundle.dir(), None, callback, state) + .await + .map_err(|e| format!("Failed to install package: {}", e))?; + } else { + todo!("Export IPA functionality"); + } - }); + Ok::<_, String>(()) + }); + + if let Err(e) = install_result { + sender_clone.send(PlumeFrameMessage::InstallProgress(100, None)).ok(); + sender_clone.send(PlumeFrameMessage::Error(format!("{}", e))).ok(); + return; } }); } }); + - - - let handler_for_import = self.frame.clone(); - let sender_for_file = sender.clone(); - let sender_for_dialog = sender.clone(); - self.default_page.set_file_handlers( - move |file_path| { - match Package::new(PathBuf::from(file_path)) { - Ok(package) => { - sender_for_file.send(PlumeFrameMessage::PackageSelected(package)).ok(); - } - Err(e) => { - sender_for_file.send(PlumeFrameMessage::Error(format!("Failed to open package: {}", e))).ok(); - } - } - }, + // MARK: Login Dialog "Next" Button + + self.login_dialog.set_next_handler({ + let frame = self.frame.clone(); + let login_dialog = self.login_dialog.clone(); move || { - let dialog = FileDialog::builder(&handler_for_import) - .with_message("Open IPA File") - .with_style(FileDialogStyle::default() | FileDialogStyle::Open) - .with_wildcard("IPA files (*.ipa;*.tipa)|*.ipa;*.tipa") - .build(); + let email = login_dialog.get_email(); + let password = login_dialog.get_password(); - if dialog.show_modal() != ID_OK { + if email.trim().is_empty() || password.is_empty() { + let dialog = MessageDialog::builder( + &frame, + "Please enter both email and password.", + "Missing Information", + ) + .with_style(MessageDialogStyle::OK | MessageDialogStyle::IconWarning) + .build(); + dialog.show_modal(); return; } - if let Some(file_path) = dialog.get_path() { - match Package::new(PathBuf::from(file_path)) { - Ok(package) => { - sender_for_dialog.send(PlumeFrameMessage::PackageSelected(package)).ok(); - } - Err(e) => { - sender_for_dialog.send(PlumeFrameMessage::Error(format!("Failed to open package: {}", e))).ok(); + + login_dialog.clear_fields(); + + thread::spawn({ + let email = email.clone(); + let password = password.clone(); + let sender = sender.clone(); + move || { + match run_login_flow(sender.clone(), &email, &password) { + Ok(account) => { + sender.send(PlumeFrameMessage::AccountLogin(account)).ok(); + + if let Err(e) = AccountCredentials.set_credentials(email, password) { + sender.send(PlumeFrameMessage::Error(format!("Failed to save credentials: {}", e))).ok(); + return; + } + }, + Err(e) => { + sender.send(PlumeFrameMessage::Error(format!("Login failed: {}", e))).ok(); + }, } } - } - }, - ); - - self.install_page.set_cancel_handler(move || { - sender.send(PlumeFrameMessage::PackageDeselected).ok(); + }); + } }); + } } + +// MARK: - Login flow + +pub fn run_login_flow( + sender: mpsc::UnboundedSender, + email: &String, + password: &String, +) -> Result { + let anisette_config = AnisetteConfiguration::default() + .set_configuration_path(get_data_path()); + + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + let (code_tx, code_rx) = std::sync::mpsc::channel::>(); + + let account_result = rt.block_on(Account::login( + || Ok((email.clone(), password.clone())), + || { + if sender + .send(PlumeFrameMessage::AwaitingTwoFactorCode(code_tx.clone())) + .is_err() + { + return Err("Failed to send 2FA request to main thread.".to_string()); + } + match code_rx.recv() { + Ok(result) => result, + Err(_) => Err("2FA process cancelled or main thread error.".to_string()), + } + }, + anisette_config, + )); + + account_result.map_err(|e| e.to_string()) +} diff --git a/apps/plumeimpactor/src/handlers.rs b/apps/plumeimpactor/src/handlers.rs index 27003e80..b8e2abcd 100644 --- a/apps/plumeimpactor/src/handlers.rs +++ b/apps/plumeimpactor/src/handlers.rs @@ -2,28 +2,41 @@ use wxdragon::prelude::*; use tokio::sync::mpsc; use tokio::sync::mpsc::error::TryRecvError; +use std::sync::mpsc as std_mpsc; -use types::{Device, Package, PlistInfoTrait}; +use grand_slam::auth::Account; +use grand_slam::utils::{PlistInfoTrait, SignerSettings}; use crate::frame::PlumeFrame; +use crate::keychain::AccountCredentials; +use crate::utils::{Device, Package}; -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum PlumeFrameMessage { DeviceConnected(Device), DeviceDisconnected(u32), PackageSelected(Package), PackageDeselected, + AccountLogin(Account), + AccountDeleted, + AwaitingTwoFactorCode(std_mpsc::Sender>), + InstallProgress(i32, Option), Error(String), } pub struct PlumeFrameMessageHandler { pub receiver: mpsc::UnboundedReceiver, pub plume_frame: PlumeFrame, + pub installation_progress_dialog: Option, // --- device --- pub usbmuxd_device_list: Vec, pub usbmuxd_selected_device_id: Option, // --- ipa --- pub package_selected: Option, + // --- account --- + pub account_credentials: Option, + // --- signer settings --- + pub signer_settings: SignerSettings, } impl PlumeFrameMessageHandler { @@ -31,15 +44,19 @@ impl PlumeFrameMessageHandler { receiver: mpsc::UnboundedReceiver, plume_frame: PlumeFrame, ) -> Self { + let signer_settings = SignerSettings::default(); Self { receiver, plume_frame, usbmuxd_device_list: Vec::new(), usbmuxd_selected_device_id: None, package_selected: None, + account_credentials: None, + installation_progress_dialog: None, + signer_settings, } } - + pub fn process_messages(&mut self) -> bool { let mut processed_count = 0; let mut has_more = false; @@ -54,7 +71,7 @@ impl PlumeFrameMessageHandler { Err(TryRecvError::Disconnected) => return false, } } - + if processed_count == 10 { has_more = true; } @@ -62,80 +79,185 @@ impl PlumeFrameMessageHandler { has_more } - fn handle_message(&mut self, message: PlumeFrameMessage) { match message { - PlumeFrameMessage::DeviceConnected(device) => { - println!("Device connected: {}", device); - if !self.usbmuxd_device_list.iter().any(|d| d.usbmuxd_device.device_id == device.usbmuxd_device.device_id) { - self.usbmuxd_device_list.push(device.clone()); - self.usbmuxd_picker_rebuild_contents(); + fn handle_message(&mut self, message: PlumeFrameMessage) { + match message { + PlumeFrameMessage::DeviceConnected(device) => { + if !self + .usbmuxd_device_list + .iter() + .any(|d| d.usbmuxd_device.device_id == device.usbmuxd_device.device_id) + { + self.usbmuxd_device_list.push(device.clone()); + self.usbmuxd_picker_rebuild_contents(); - if self.usbmuxd_device_list.len() == 1 { - self.usbmuxd_picker_select_item(&device.usbmuxd_device.device_id); - } else { + if self.usbmuxd_device_list.len() == 1 { + self.usbmuxd_picker_select_item(&device.usbmuxd_device.device_id); + } else { + self.usbmuxd_picker_reconcile_selection(); + } + } + + self.plume_frame.install_page.install_button.enable(true); + } + PlumeFrameMessage::DeviceDisconnected(device_id) => { + if let Some(index) = self + .usbmuxd_device_list + .iter() + .position(|d| d.usbmuxd_device.device_id == device_id) + { + self.usbmuxd_device_list.remove(index); + self.usbmuxd_picker_rebuild_contents(); self.usbmuxd_picker_reconcile_selection(); } + + if self.usbmuxd_device_list.is_empty() { + self.plume_frame.install_page.install_button.enable(false); + } } - } - PlumeFrameMessage::DeviceDisconnected(device_id) => { - println!("Device disconnected: {}", device_id); - if let Some(index) = self.usbmuxd_device_list.iter().position(|d| d.usbmuxd_device.device_id == device_id) { - self.usbmuxd_device_list.remove(index); - self.usbmuxd_picker_rebuild_contents(); - self.usbmuxd_picker_reconcile_selection(); + PlumeFrameMessage::PackageSelected(package) => { + if self.package_selected.is_some() { + return; + } + + package.load_into_signer_settings(&mut self.signer_settings); + + self.package_selected = Some(package); + self.plume_frame.install_page.set_settings(&self.signer_settings, Some(self.package_selected.as_ref().unwrap())); + self.plume_frame.default_page.panel.hide(); + self.plume_frame.install_page.panel.show(true); + self.plume_frame.frame.layout(); + + self.plume_frame.add_ipa_button.enable(false); } - } - PlumeFrameMessage::PackageSelected(package) => { - if self.package_selected.is_some() { - return; + PlumeFrameMessage::PackageDeselected => { + // TODO: should it be this way? + if let Some(package) = self.package_selected.as_ref() { + package.clone().remove_package_stage(); + } + + self.package_selected = None; + self.plume_frame.install_page.panel.hide(); + self.plume_frame.default_page.panel.show(true); + self.plume_frame.frame.layout(); + self.signer_settings = SignerSettings::default(); + self.plume_frame.install_page.set_settings(&self.signer_settings, None); + self.plume_frame.add_ipa_button.enable(true); } - - let package_name = package.get_name().unwrap_or_else(|| "Unknown".to_string()); - let package_id = package.get_bundle_identifier().unwrap_or_else(|| "Unknown".to_string()); - println!("Package selected: {}", package_name); - self.package_selected = Some(package); - self.plume_frame.install_page.set_top_text(format!("{} - {}", package_name, package_id).as_str()); - self.plume_frame.default_page.panel.hide(); - self.plume_frame.install_page.panel.show(true); - self.plume_frame.frame.layout(); - } - PlumeFrameMessage::PackageDeselected => { - println!("Package deselected"); - self.package_selected = None; - self.plume_frame.install_page.panel.hide(); - self.plume_frame.default_page.panel.show(true); - self.plume_frame.frame.layout(); - } - PlumeFrameMessage::Error(error_msg) => { - println!("Error: {}", error_msg); - let dialog = MessageDialog::builder(&self.plume_frame.frame, &error_msg, "Error") - .with_style(MessageDialogStyle::OK | MessageDialogStyle::IconWarning) + PlumeFrameMessage::AccountLogin(account) => { + let (first, last) = account.get_name(); + let dialog = MessageDialog::builder( + &self.plume_frame.frame, + &format!("Logged in as {} {}", first, last), + "Signed In" + ) + .with_style(MessageDialogStyle::OK | MessageDialogStyle::IconInformation) .build(); - dialog.show_modal(); + dialog.show_modal(); + self.account_credentials = Some(account); + + self.plume_frame.login_dialog.dialog.hide(); + self.plume_frame.settings_dialog.set_account_name(Some((first, last))); + } + PlumeFrameMessage::AccountDeleted => { + if self.account_credentials.is_none() { + return; + } + + let creds = AccountCredentials; + if let Err(e) = creds.delete_password() { + self.handle_message(PlumeFrameMessage::Error(format!("Failed to delete account credentials: {}", e))); + return; + } + + self.account_credentials = None; + self.plume_frame.settings_dialog.set_account_name(None); + } + PlumeFrameMessage::AwaitingTwoFactorCode(tx) => { + let result = self.plume_frame.create_single_field_dialog( + "Two-Factor Authentication", + "Enter the verification code sent to your device:", + ); + + if let Err(e) = tx.send(result) { + self.handle_message(PlumeFrameMessage::Error(format!("Failed to send two-factor code response: {}", e))); + } + } + PlumeFrameMessage::InstallProgress(progress, message_opt) => { + println!("Progress: {} - {:?}", progress, message_opt); + let Some(selected_package) = &self.package_selected else { + return; + }; + + if self.installation_progress_dialog.is_none() { + let progress_dialog = ProgressDialog::builder( + &self.plume_frame.frame, + &format!("Installing {}", selected_package.get_name().unwrap_or("Unknown".to_string())), + "Waiting...", + 100 + ) + .show_elapsed_time() + .show_estimated_time() + .show_remaining_time() + .can_abort() + .smooth() + .build(); + + self.installation_progress_dialog = Some(progress_dialog); + } + + if let Some(dialog) = &mut self.installation_progress_dialog { + dialog.update(progress, message_opt.as_deref()); + + if progress >= 90 { + self.installation_progress_dialog = None; + } + } + } + PlumeFrameMessage::Error(error_msg) => { + let dialog = MessageDialog::builder(&self.plume_frame.frame, &error_msg, "Error") + .with_style(MessageDialogStyle::OK | MessageDialogStyle::IconWarning) + .build(); + dialog.show_modal(); + } } - }} + } +} + +// USBMUXD HANDLERS - // --- Device Picker Helpers --- - +impl PlumeFrameMessageHandler { fn usbmuxd_picker_rebuild_contents(&self) { self.plume_frame.usbmuxd_picker.clear(); for item_string in &self.usbmuxd_device_list { - self.plume_frame.usbmuxd_picker.append(&item_string.to_string()); + self.plume_frame + .usbmuxd_picker + .append(&item_string.to_string()); } } fn usbmuxd_picker_select_item(&mut self, device_id: &u32) { - if let Some(index) = self.usbmuxd_device_list.iter().position(|d| d.usbmuxd_device.device_id == *device_id) { + if let Some(index) = self + .usbmuxd_device_list + .iter() + .position(|d| d.usbmuxd_device.device_id == *device_id) + { self.plume_frame.usbmuxd_picker.set_selection(index as u32); self.usbmuxd_selected_device_id = Some(device_id.to_string()); } else { self.usbmuxd_selected_device_id = None; } } - + fn usbmuxd_picker_reconcile_selection(&mut self) { if let Some(selected_item) = self.usbmuxd_selected_device_id.clone() { - if let Some(new_index) = self.usbmuxd_device_list.iter().position(|d| d.usbmuxd_device.device_id.to_string() == selected_item) { - self.plume_frame.usbmuxd_picker.set_selection(new_index as u32); + if let Some(new_index) = self + .usbmuxd_device_list + .iter() + .position(|d| d.usbmuxd_device.device_id.to_string() == selected_item) + { + self.plume_frame + .usbmuxd_picker + .set_selection(new_index as u32); } else { self.usbmuxd_picker_default_selection(); } diff --git a/apps/plumeimpactor/src/keychain.rs b/apps/plumeimpactor/src/keychain.rs index 0a316d1c..8b40681e 100644 --- a/apps/plumeimpactor/src/keychain.rs +++ b/apps/plumeimpactor/src/keychain.rs @@ -1,7 +1,35 @@ -#[cfg(all(target_os = "macos", not(debug_assertions)))] -pub use keyring_core::Entry; -#[cfg(not(all(target_os = "macos", not(debug_assertions))))] -pub use keyring::Entry; +use keyring::{Entry, Error}; -const KEYRING_SERVICE: &'static str = "Plume Impactor Credentials"; -const KEYRING_USER: &'static str = "Apple ID"; +const KEYRING_SERVICE: &str = env!("CARGO_PKG_NAME"); +const KEYRING_EMAIL: &str = "Apple ID Email"; +const KEYRING_PASS: &str = "Apple ID Password"; + +pub struct AccountCredentials; + +impl AccountCredentials { + pub fn set_credentials(&self, email: String, password: String) -> Result<(), Error> { + let entry_email = Entry::new(KEYRING_SERVICE, KEYRING_EMAIL)?; + let entry_pass = Entry::new(KEYRING_SERVICE, KEYRING_PASS)?; + entry_email.set_secret(email.as_bytes())?; + entry_pass.set_secret(password.as_bytes())?; + Ok(()) + } + + pub fn get_email(&self) -> Result { + let entry = Entry::new(KEYRING_SERVICE, KEYRING_EMAIL)?; + entry.get_password() + } + + pub fn get_password(&self) -> Result { + let entry = Entry::new(KEYRING_SERVICE, KEYRING_PASS)?; + entry.get_password() + } + + pub fn delete_password(&self) -> Result<(), Error> { + let entry_email = Entry::new(KEYRING_SERVICE, KEYRING_EMAIL)?; + let entry_pass = Entry::new(KEYRING_SERVICE, KEYRING_PASS)?; + entry_email.delete_credential()?; + entry_pass.delete_credential()?; + Ok(()) + } +} diff --git a/apps/plumeimpactor/src/main.rs b/apps/plumeimpactor/src/main.rs index d3ce6f7d..0a2304aa 100644 --- a/apps/plumeimpactor/src/main.rs +++ b/apps/plumeimpactor/src/main.rs @@ -1,13 +1,50 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + mod frame; mod keychain; mod pages; mod handlers; +mod utils; -pub const APP_NAME: &str = concat!(env!("CARGO_PKG_NAME"), " – Version ", env!("CARGO_PKG_VERSION")); +use std::{ + env, + fs, + path::{Path, PathBuf} +}; #[tokio::main] async fn main() { + _ = rustls::crypto::ring::default_provider().install_default().unwrap(); + let _ = wxdragon::main(|_| { frame::PlumeFrame::new().show(); }); } + +use thiserror::Error as ThisError; + +#[derive(Debug, ThisError)] +pub enum Error { + #[error("Info.plist not found")] + PackageInfoPlistMissing, + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + #[error("Plist error: {0}")] + Plist(#[from] plist::Error), + #[error("Zip error: {0}")] + Zip(#[from] zip::result::ZipError), + #[error("Idevice error: {0}")] + Idevice(#[from] idevice::IdeviceError), + #[error("GrandSlam error: {0}")] + GrandSlam(#[from] grand_slam::Error), +} + +pub fn get_data_path() -> PathBuf { + let dir = Path::new(&env::var("HOME").unwrap()) + .join(".config") + .join("PlumeImpactor"); + + fs::create_dir_all(&dir).ok(); + + dir +} diff --git a/apps/plumeimpactor/src/pages/default.rs b/apps/plumeimpactor/src/pages/default.rs index 4e6c136d..471f70bc 100644 --- a/apps/plumeimpactor/src/pages/default.rs +++ b/apps/plumeimpactor/src/pages/default.rs @@ -1,8 +1,9 @@ use wxdragon::prelude::*; -const INSTALLER_IMAGE_BYTES: &[u8] = include_bytes!("../../resources/install.png"); -const INSTALLER_IMAGE_DIMENSIONS: u32 = 100; +#[cfg(not(target_os = "linux"))] const WELCOME_TEXT: &str = "Drop your .ipa here"; +#[cfg(target_os = "linux")] +const WELCOME_TEXT: &str = "Press 'import' and select an .ipa to get started"; #[derive(Clone)] pub struct DefaultPage { @@ -10,12 +11,13 @@ pub struct DefaultPage { } impl DefaultPage { + #[cfg(not(target_os = "linux"))] fn is_allowed_file(path: &str) -> bool { path.ends_with(".ipa") || path.ends_with(".tipa") } - // it seems that image is on top of the panel... - pub fn set_file_handlers(&self, on_drop: impl Fn(String) + 'static, on_click: impl Fn() + 'static) { + #[cfg(not(target_os = "linux"))] + pub fn set_file_handlers(&self, on_drop: impl Fn(String) + 'static) { _ = FileDropTarget::builder(&self.panel) .with_on_drop_files(move |files, _, _| { if files.len() != 1 || !DefaultPage::is_allowed_file(&files[0]) { @@ -27,13 +29,7 @@ impl DefaultPage { .with_on_drag_over(move |_, _, _| DragResult::Move) .with_on_enter(move |_, _, _| DragResult::Move) .build(); - - self.panel.on_mouse_left_down(move |_evt| { - on_click(); - }); } - - } pub fn create_default_page(frame: &Frame) -> DefaultPage { @@ -41,41 +37,22 @@ pub fn create_default_page(frame: &Frame) -> DefaultPage { let sizer = BoxSizer::builder(Orientation::Vertical).build(); sizer.add_stretch_spacer(1); - - if let Ok(img) = image::load_from_memory_with_format(INSTALLER_IMAGE_BYTES, image::ImageFormat::Png) { - let resized = img.resize_exact( - INSTALLER_IMAGE_DIMENSIONS, - INSTALLER_IMAGE_DIMENSIONS, - image::imageops::FilterType::Lanczos3, - ); - let rgba = resized.to_rgba8(); - let bitmap = Bitmap::from_rgba( - rgba.as_raw(), - INSTALLER_IMAGE_DIMENSIONS, - INSTALLER_IMAGE_DIMENSIONS, - ); - let static_bitmap = StaticBitmap::builder(&panel) - .with_bitmap(bitmap) - .with_size(Size::new( - INSTALLER_IMAGE_DIMENSIONS as i32, - INSTALLER_IMAGE_DIMENSIONS as i32, - )) - .build(); - sizer.add(&static_bitmap, 0, SizerFlag::AlignCenterHorizontal | SizerFlag::All, 20); - } let welcome_text = StaticText::builder(&panel) .with_label(WELCOME_TEXT) .with_style(StaticTextStyle::AlignCenterHorizontal) .build(); - sizer.add(&welcome_text, 0, SizerFlag::AlignCenterHorizontal | SizerFlag::All, 0); + sizer.add( + &welcome_text, + 0, + SizerFlag::AlignCenterHorizontal | SizerFlag::All, + 0, + ); - sizer.add_stretch_spacer(2); + sizer.add_stretch_spacer(1); panel.set_sizer(sizer, true); - DefaultPage { - panel - } + DefaultPage { panel } } diff --git a/apps/plumeimpactor/src/pages/install.rs b/apps/plumeimpactor/src/pages/install.rs index 6fabd01d..f658a644 100644 --- a/apps/plumeimpactor/src/pages/install.rs +++ b/apps/plumeimpactor/src/pages/install.rs @@ -1,10 +1,28 @@ +use grand_slam::utils::{PlistInfoTrait, SignerSettings}; use wxdragon::prelude::*; +use crate::utils::Package; + #[derive(Clone)] pub struct InstallPage { pub panel: Panel, pub cancel_button: Button, - pub top_text: StaticText, + pub install_button: Button, + + custom_name_textfield: TextCtrl, + custom_identifier_textfield: TextCtrl, + custom_version_textfield: TextCtrl, + support_older_versions_checkbox: CheckBox, + support_file_sharing_checkbox: CheckBox, + ipad_fullscreen_checkbox: CheckBox, + game_mode_checkbox: CheckBox, + pro_motion_checkbox: CheckBox, + should_embed_pairing_checkbox: CheckBox, + skip_registering_extensions_checkbox: CheckBox, + + original_name: Option, + original_identifier: Option, + original_version: Option, } pub fn create_install_page(frame: &Frame) -> InstallPage { @@ -12,10 +30,77 @@ pub fn create_install_page(frame: &Frame) -> InstallPage { let main_sizer = BoxSizer::builder(Orientation::Vertical).build(); - let top_text = StaticText::builder(&panel) - .with_label("Unknown") + let settings_sizer = BoxSizer::builder(Orientation::Horizontal).build(); + + let textfields_sizer = BoxSizer::builder(Orientation::Vertical).build(); + let bundle_name_label = StaticText::builder(&panel) + .with_label("Name:") + .build(); + let custom_name_textfield = TextCtrl::builder(&panel) + .with_value("") + .build(); + let bundle_identifier_label = StaticText::builder(&panel) + .with_label("Identifier:") .build(); - main_sizer.add(&top_text, 0, SizerFlag::Left | SizerFlag::Top, 10); + let custom_identifier_textfield = TextCtrl::builder(&panel) + .with_value("") + .build(); + let bundle_version_label = StaticText::builder(&panel) + .with_label("Version:") + .build(); + let custom_version_textfield = TextCtrl::builder(&panel) + .with_value("") + .build(); + textfields_sizer.add(&bundle_name_label, 0, SizerFlag::Bottom, 6); + textfields_sizer.add(&custom_name_textfield, 0, SizerFlag::Expand | SizerFlag::Left, 8); + textfields_sizer.add(&bundle_identifier_label, 0, SizerFlag::Top | SizerFlag::Bottom, 6); + textfields_sizer.add(&custom_identifier_textfield, 0, SizerFlag::Expand | SizerFlag::Left, 8); + textfields_sizer.add(&bundle_version_label, 0, SizerFlag::Top | SizerFlag::Bottom, 6); + textfields_sizer.add(&custom_version_textfield, 0, SizerFlag::Expand | SizerFlag::Left, 8); + + let checkbox_sizer = BoxSizer::builder(Orientation::Vertical).build(); + let general_label = StaticText::builder(&panel) + .with_label("General:") + .build(); + let support_older_versions_checkbox = CheckBox::builder(&panel) + .with_label("Try to support older versions (7+)") + .build(); + let support_file_sharing_checkbox = CheckBox::builder(&panel) + .with_label("Force File Sharing") + .build(); + let ipad_fullscreen_checkbox = CheckBox::builder(&panel) + .with_label("Force iPad Fullscreen") + .build(); + let game_mode_checkbox = CheckBox::builder(&panel) + .with_label("Force Game Mode") + .build(); + let pro_motion_checkbox = CheckBox::builder(&panel) + .with_label("Force Pro Motion") + .build(); + let advanced_label = StaticText::builder(&panel) + .with_label("Advanced:") + .build(); + let should_embed_pairing_checkbox = CheckBox::builder(&panel) + .with_label("Embed Pairing File") + .build(); + should_embed_pairing_checkbox.enable(false); + let skip_registering_extensions_checkbox = CheckBox::builder(&panel) + .with_label("Only Register Main Bundle") + .build(); + checkbox_sizer.add(&general_label, 0, SizerFlag::Bottom, 6); + checkbox_sizer.add(&support_older_versions_checkbox, 0, SizerFlag::Expand | SizerFlag::Left, 8); + checkbox_sizer.add(&support_file_sharing_checkbox, 0, SizerFlag::Expand | SizerFlag::Top | SizerFlag::Left, 8); + checkbox_sizer.add(&ipad_fullscreen_checkbox, 0, SizerFlag::Expand | SizerFlag::Top | SizerFlag::Left, 8); + checkbox_sizer.add(&game_mode_checkbox, 0, SizerFlag::Expand | SizerFlag::Top | SizerFlag::Left, 8); + checkbox_sizer.add(&pro_motion_checkbox, 0, SizerFlag::Expand | SizerFlag::Top | SizerFlag::Left | SizerFlag::Bottom, 8); + checkbox_sizer.add(&advanced_label, 0, SizerFlag::Top | SizerFlag::Bottom, 6); + checkbox_sizer.add(&should_embed_pairing_checkbox, 0, SizerFlag::Expand | SizerFlag::Left, 8); + checkbox_sizer.add(&skip_registering_extensions_checkbox, 0, SizerFlag::Expand | SizerFlag::Top | SizerFlag::Left, 8); + + settings_sizer.add_sizer(&textfields_sizer, 1, SizerFlag::Expand | SizerFlag::Right, 13); + settings_sizer.add_sizer(&checkbox_sizer, 1, SizerFlag::Expand, 13); + + main_sizer.add_sizer(&settings_sizer, 0, SizerFlag::Expand | SizerFlag::Left | SizerFlag::Right, 13); main_sizer.add_stretch_spacer(1); @@ -24,23 +109,120 @@ pub fn create_install_page(frame: &Frame) -> InstallPage { let cancel_button = Button::builder(&panel) .with_label("Cancel") .build(); - let install_button = Button::builder(&panel) .with_label("Install") .build(); + install_button.enable(false); button_sizer.add_stretch_spacer(1); - button_sizer.add(&cancel_button, 0, SizerFlag::Right, 8); + button_sizer.add(&cancel_button, 0, SizerFlag::Right, 13); button_sizer.add(&install_button, 0, SizerFlag::All, 0); - main_sizer.add_sizer(&button_sizer, 0, SizerFlag::Right | SizerFlag::Bottom | SizerFlag::Expand, 10); + main_sizer.add_sizer( + &button_sizer, + 0, + SizerFlag::Right | SizerFlag::Bottom | SizerFlag::Expand, + 13, + ); panel.set_sizer(main_sizer, true); InstallPage { panel, cancel_button, - top_text, + install_button, + + custom_name_textfield, + custom_identifier_textfield, + custom_version_textfield, + support_older_versions_checkbox, + support_file_sharing_checkbox, + ipad_fullscreen_checkbox, + game_mode_checkbox, + pro_motion_checkbox, + should_embed_pairing_checkbox, + skip_registering_extensions_checkbox, + + original_name: None, + original_identifier: None, + original_version: None, + } +} + + +impl InstallPage { + pub fn set_settings(&mut self, settings: &SignerSettings, package: Option<&Package>) { + self.support_older_versions_checkbox.set_value(settings.support_minimum_os_version); + self.support_file_sharing_checkbox.set_value(settings.support_file_sharing); + self.ipad_fullscreen_checkbox.set_value(settings.support_ipad_fullscreen); + self.game_mode_checkbox.set_value(settings.support_game_mode); + self.pro_motion_checkbox.set_value(settings.support_pro_motion); + self.should_embed_pairing_checkbox.set_value(settings.should_embed_pairing); + self.skip_registering_extensions_checkbox.set_value(settings.should_only_use_main_provisioning); + + if let Some(package) = package { + if let Some(ref name) = package.get_name() { + self.custom_name_textfield.set_value(name); + self.original_name = Some(name.clone()); + } else { + self.custom_name_textfield.set_value(""); + self.original_name = None; + } + + if let Some(ref identifier) = package.get_bundle_identifier() { + self.custom_identifier_textfield.set_value(identifier); + self.original_identifier = Some(identifier.clone()); + } else { + self.custom_identifier_textfield.set_value(""); + self.original_identifier = None; + } + + if let Some(ref version) = package.get_version() { + self.custom_version_textfield.set_value(version); + self.original_version = Some(version.clone()); + } else { + self.custom_version_textfield.set_value(""); + self.original_version = None; + } + } else { + self.custom_name_textfield.set_value(""); + self.custom_identifier_textfield.set_value(""); + self.custom_version_textfield.set_value(""); + self.original_name = None; + self.original_identifier = None; + self.original_version = None; + } + } + + pub fn update_fields(&self, settings: &mut SignerSettings) { + settings.support_minimum_os_version = self.support_older_versions_checkbox.get_value(); + settings.support_file_sharing = self.support_file_sharing_checkbox.get_value(); + settings.support_ipad_fullscreen = self.ipad_fullscreen_checkbox.get_value(); + settings.support_game_mode = self.game_mode_checkbox.get_value(); + settings.support_pro_motion = self.pro_motion_checkbox.get_value(); + settings.should_embed_pairing = self.should_embed_pairing_checkbox.get_value(); + settings.should_only_use_main_provisioning = self.skip_registering_extensions_checkbox.get_value(); + + if let Some(ref original_name) = self.original_name { + let current_name = self.custom_name_textfield.get_value(); + if ¤t_name != original_name { + settings.custom_name = Some(current_name.to_string()); + } + } + + if let Some(ref original_identifier) = self.original_identifier { + let current_identifier = self.custom_identifier_textfield.get_value(); + if ¤t_identifier != original_identifier { + settings.custom_identifier = Some(current_identifier.to_string()); + } + } + + if let Some(ref original_version) = self.original_version { + let current_version = self.custom_version_textfield.get_value(); + if ¤t_version != original_version { + settings.custom_version = Some(current_version.to_string()); + } + } } } @@ -51,7 +233,9 @@ impl InstallPage { }); } - pub fn set_top_text(&self, text: &str) { - self.top_text.set_label(text); + pub fn set_install_handler(&self, on_install: impl Fn() + 'static) { + self.install_button.on_click(move |_evt| { + on_install(); + }); } } diff --git a/apps/plumeimpactor/src/pages/login.rs b/apps/plumeimpactor/src/pages/login.rs deleted file mode 100644 index 07b6e895..00000000 --- a/apps/plumeimpactor/src/pages/login.rs +++ /dev/null @@ -1,118 +0,0 @@ -use wxdragon::prelude::*; - -#[derive(Clone)] -pub struct LoginDialog { - pub dialog: Dialog, - pub email_field: TextCtrl, - pub password_field: TextCtrl, - pub cancel_button: Button, - pub next_button: Button, -} - -pub fn create_login_dialog(parent: &Window) -> LoginDialog { - let dialog = Dialog::builder(parent, "Sign in with your Apple ID") - .with_style(DialogStyle::DefaultDialogStyle) - .build(); - - let sizer = BoxSizer::builder(Orientation::Vertical).build(); - - sizer.add_spacer(16); - - let email_label = StaticText::builder(&dialog) - .with_label("Email:") - .build(); - let email_field = TextCtrl::builder(&dialog).build(); - sizer.add(&email_label, 0, SizerFlag::All, 8); - sizer.add(&email_field, 0, SizerFlag::Expand | SizerFlag::All, 8); - - let password_label = StaticText::builder(&dialog) - .with_label("Password:") - .build(); - let password_field = TextCtrl::builder(&dialog) - .with_style(TextCtrlStyle::Password) - .build(); - sizer.add(&password_label, 0, SizerFlag::All, 8); - sizer.add(&password_field, 0, SizerFlag::Expand | SizerFlag::All, 8); - - let button_sizer = BoxSizer::builder(Orientation::Horizontal).build(); - let cancel_button = Button::builder(&dialog) - .with_label("Cancel") - .build(); - let next_button = Button::builder(&dialog) - .with_label("Next") - .build(); - button_sizer.add(&cancel_button, 0, SizerFlag::All, 8); - button_sizer.add_spacer(16); - button_sizer.add(&next_button, 0, SizerFlag::All, 8); - - sizer.add_spacer(16); - sizer.add_sizer(&button_sizer, 0, SizerFlag::AlignRight | SizerFlag::All, 8); - - dialog.set_sizer(sizer, true); - - LoginDialog { - dialog, - email_field, - password_field, - cancel_button, - next_button, - } -} - -impl LoginDialog { - pub fn get_email(&self) -> String { - self.email_field.get_value().to_string() - } - - pub fn get_password(&self) -> String { - self.password_field.get_value().to_string() - } - - pub fn clear_fields(&self) { - self.email_field.set_value(""); - self.password_field.set_value(""); - } - - pub fn show_modal(&self) { - self.dialog.show_modal(); - } - - pub fn hide(&self) { - self.dialog.end_modal(0); - } - - pub fn set_cancel_handler(&self, on_cancel: impl Fn() + 'static) { - self.cancel_button.on_click(move |_evt| { - on_cancel(); - }); - } - - pub fn set_next_handler(&self, on_next: impl Fn() + 'static) { - self.next_button.on_click(move |_evt| { - on_next(); - }); - } -} - -pub fn create_single_field_dialog(parent: &Window, title: &str, label: &str) -> Result { - let dialog = Dialog::builder(parent, title) - .with_style(DialogStyle::DefaultDialogStyle) - .build(); - - let sizer = BoxSizer::builder(Orientation::Vertical).build(); - sizer.add_spacer(16); - - let field_label = StaticText::builder(&dialog) - .with_label(label) - .build(); - let text_field = TextCtrl::builder(&dialog).build(); - sizer.add(&field_label, 0, SizerFlag::All, 8); - sizer.add(&text_field, 0, SizerFlag::Expand | SizerFlag::All, 8); - - dialog.set_sizer(sizer, true); - - dialog.show_modal(); - let value = text_field.get_value().to_string(); - dialog.destroy(); - Ok(value) -} diff --git a/apps/plumeimpactor/src/pages/mod.rs b/apps/plumeimpactor/src/pages/mod.rs index fe9846c9..542a2ecd 100644 --- a/apps/plumeimpactor/src/pages/mod.rs +++ b/apps/plumeimpactor/src/pages/mod.rs @@ -4,5 +4,18 @@ pub use default::{DefaultPage, create_default_page}; pub mod install; pub use install::{InstallPage, create_install_page}; -pub mod login; -pub use login::create_login_dialog; +pub mod settings; +pub use settings::{LoginDialog, create_login_dialog}; +pub use settings::{SettingsDialog, create_settings_dialog}; + +// TODO: investigate why github actions messes up weird sizing shit +#[cfg(target_os = "linux")] +pub const WINDOW_SIZE: (i32, i32) = (700, 660); +#[cfg(not(target_os = "linux"))] +pub const WINDOW_SIZE: (i32, i32) = (530, 410); + +// TODO: investigate why github actions messes up weird sizing shit +#[cfg(target_os = "linux")] +pub const DIALOG_SIZE: (i32, i32) = (500, 500); +#[cfg(not(target_os = "linux"))] +pub const DIALOG_SIZE: (i32, i32) = (400, 300); diff --git a/apps/plumeimpactor/src/pages/settings.rs b/apps/plumeimpactor/src/pages/settings.rs new file mode 100644 index 00000000..ae10863a --- /dev/null +++ b/apps/plumeimpactor/src/pages/settings.rs @@ -0,0 +1,208 @@ +use wxdragon::prelude::*; + +use crate::frame::PlumeFrame; +use super::DIALOG_SIZE; + +#[derive(Clone)] +pub struct LoginDialog { + pub dialog: Dialog, + pub email_field: TextCtrl, + pub password_field: TextCtrl, + pub next_button: Button, +} + +pub fn create_login_dialog(parent: &Window) -> LoginDialog { + let dialog = Dialog::builder(parent, "Sign in with your Apple ID") + .with_style(DialogStyle::SystemMenu | DialogStyle::Caption) + .with_size(DIALOG_SIZE.0, DIALOG_SIZE.1) + .build(); + + let sizer = BoxSizer::builder(Orientation::Vertical).build(); + sizer.add_spacer(13); + + let email_row = BoxSizer::builder(Orientation::Horizontal).build(); + let email_label = StaticText::builder(&dialog) + .with_label(" Email:") + .build(); + let email_field = TextCtrl::builder(&dialog).build(); + email_row.add(&email_label, 0, SizerFlag::AlignCenterVertical | SizerFlag::All, 4); + email_row.add(&email_field, 1, SizerFlag::Expand | SizerFlag::Right, 8); + sizer.add_sizer(&email_row, 0, SizerFlag::Expand | SizerFlag::All, 4); + + let password_row = BoxSizer::builder(Orientation::Horizontal).build(); + let password_label = StaticText::builder(&dialog).with_label("Password:").build(); + let password_field = TextCtrl::builder(&dialog) + .with_style(TextCtrlStyle::Password) + .build(); + password_row.add(&password_label, 0, SizerFlag::AlignCenterVertical | SizerFlag::All, 4); + password_row.add(&password_field, 1, SizerFlag::Expand | SizerFlag::Right, 8); + sizer.add_sizer(&password_row, 0, SizerFlag::Expand | SizerFlag::All, 4); + + let button_sizer = BoxSizer::builder(Orientation::Horizontal).build(); + let cancel_button = Button::builder(&dialog).with_label("Cancel").build(); + let next_button = Button::builder(&dialog).with_label("Next").build(); + button_sizer.add(&cancel_button, 1, SizerFlag::Expand | SizerFlag::All, 0); + button_sizer.add_spacer(13); + button_sizer.add(&next_button, 1, SizerFlag::Expand | SizerFlag::All, 0); + + sizer.add_sizer(&button_sizer, 0, SizerFlag::AlignRight | SizerFlag::All, 13); + + dialog.set_sizer(sizer, true); + + cancel_button.on_click({ + let dialog = dialog.clone(); + move |_| dialog.end_modal(ID_CANCEL as i32) + }); + + LoginDialog { + dialog, + email_field, + password_field, + next_button, + } +} + +impl LoginDialog { + pub fn get_email(&self) -> String { + self.email_field.get_value().to_string() + } + + pub fn get_password(&self) -> String { + self.password_field.get_value().to_string() + } + + pub fn clear_fields(&self) { + self.email_field.set_value(""); + self.password_field.set_value(""); + } + + pub fn set_next_handler(&self, on_next: impl Fn() + 'static) { + self.next_button.on_click(move |_evt| { + on_next(); + }); + } +} + +// MARK: - AccountDialog + +#[derive(Clone)] +pub struct SettingsDialog { + pub dialog: Dialog, + pub logout_button: Button, + pub account_label: StaticText, +} + +pub fn create_settings_dialog(parent: &Window) -> SettingsDialog { + let dialog = Dialog::builder(parent, "Settings") + .with_size(DIALOG_SIZE.0, DIALOG_SIZE.1) + .build(); + + let sizer = BoxSizer::builder(Orientation::Vertical).build(); + sizer.add_spacer(13); + + let account_row = BoxSizer::builder(Orientation::Horizontal).build(); + let account_label = StaticText::builder(&dialog).with_label("Not logged in").build(); + let logout_button = Button::builder(&dialog).with_label("Login").build(); + account_row.add(&account_label, 4, SizerFlag::Expand, 0); + account_row.add_stretch_spacer(1); + account_row.add(&logout_button, 1, SizerFlag::Expand, 0); + + sizer.add_sizer(&account_row, 0, SizerFlag::Right | SizerFlag::Left, 13); + + sizer.add(&StaticLine::builder(&dialog).build(), 0, SizerFlag::Expand | SizerFlag::All, 13); + + let cert_button_sizer = BoxSizer::builder(Orientation::Horizontal).build(); + let import_cert_button = Button::builder(&dialog).with_label("Import P12").build(); + import_cert_button.enable(false); + let export_cert_button = Button::builder(&dialog).with_label("Export P12").build(); + export_cert_button.enable(false); + cert_button_sizer.add(&import_cert_button, 1, SizerFlag::Expand, 0); + cert_button_sizer.add_spacer(13); + cert_button_sizer.add(&export_cert_button, 1, SizerFlag::Expand, 0); + + sizer.add_sizer(&cert_button_sizer, 0, SizerFlag::Right | SizerFlag::Left, 13); + + dialog.set_sizer(sizer, true); + + SettingsDialog { + dialog, + logout_button, + account_label, + } +} + +impl SettingsDialog { + pub fn set_logout_handler(&self, on_logout: impl Fn() + 'static) { + self.logout_button.on_click(move |_| { + on_logout(); + }); + } + + pub fn set_account_name(&self, account_name: Option<(String, String)>) { + match account_name { + Some((first, last)) => { + self.account_label.set_label(&format!("Logged in as {} {}", first, last)); + self.logout_button.set_label("Logout"); + } + None => { + self.account_label.set_label("Not logged in"); + self.logout_button.set_label("Sign In"); + } + } + } +} + +// MARK: - Single Field Dialog +impl PlumeFrame { + pub fn create_single_field_dialog(&self, title: &str, label: &str) -> Result { + let dialog = Dialog::builder(&self.frame, title) + .with_style(DialogStyle::SystemMenu | DialogStyle::Caption) + .with_size(DIALOG_SIZE.0, DIALOG_SIZE.1) + .build(); + + let sizer = BoxSizer::builder(Orientation::Vertical).build(); + sizer.add_spacer(16); + + sizer.add( + &StaticText::builder(&dialog).with_label(label).build(), + 0, + SizerFlag::All, + 12, + ); + let text_field = TextCtrl::builder(&dialog).build(); + sizer.add(&text_field, 0, SizerFlag::Expand | SizerFlag::All, 8); + + let button_sizer = BoxSizer::builder(Orientation::Horizontal).build(); + + let cancel_button = Button::builder(&dialog).with_label("Cancel").build(); + let ok_button = Button::builder(&dialog).with_label("OK").build(); + + button_sizer.add(&cancel_button, 0, SizerFlag::All, 8); + button_sizer.add_spacer(8); + button_sizer.add(&ok_button, 0, SizerFlag::All, 8); + + sizer.add_sizer(&button_sizer, 0, SizerFlag::AlignRight | SizerFlag::All, 8); + + dialog.set_sizer(sizer, true); + + cancel_button.on_click({ + let dialog = dialog.clone(); + move |_| dialog.end_modal(ID_CANCEL as i32) + }); + ok_button.on_click({ + let dialog = dialog.clone(); + move |_| dialog.end_modal(ID_OK as i32) + }); + + text_field.set_focus(); + + let rc = dialog.show_modal(); + let result = if rc == ID_OK as i32 { + Ok(text_field.get_value().to_string()) + } else { + Err("2FA cancelled".to_string()) + }; + dialog.destroy(); + result + } +} diff --git a/crates/types/src/device.rs b/apps/plumeimpactor/src/utils/device.rs similarity index 62% rename from crates/types/src/device.rs rename to apps/plumeimpactor/src/utils/device.rs index 4dd9d320..815f684c 100644 --- a/crates/types/src/device.rs +++ b/apps/plumeimpactor/src/utils/device.rs @@ -4,24 +4,48 @@ use idevice::usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdDevice}; use idevice::lockdown::LockdownClient; use idevice::IdeviceService; -use errors::Error; +use crate::Error; -pub const CONNECTION_LABEL: &str = "plume"; +pub const CONNECTION_LABEL: &str = "plume_info"; + +macro_rules! get_dict_string { + ($dict:expr, $key:expr) => { + $dict + .as_dictionary() + .and_then(|dict| dict.get($key)) + .and_then(|v| v.as_string()) + .map(|s| s.to_string()) + .unwrap_or_else(|| "".to_string()) + }; +} #[derive(Debug, Clone)] pub struct Device { pub name: String, + pub uuid: String, pub usbmuxd_device: UsbmuxdDevice, } impl Device { pub async fn new(usbmuxd_device: UsbmuxdDevice) -> Self { - let name = get_name_from_usbmuxd_device(&usbmuxd_device).await.unwrap_or_default(); - Device { - name, + let name = Self::get_name_from_usbmuxd_device(&usbmuxd_device) + .await + .unwrap_or_default(); + + Device { + name, + uuid: usbmuxd_device.udid.clone(), usbmuxd_device } } + + async fn get_name_from_usbmuxd_device( + device: &UsbmuxdDevice, + ) -> Result { + let mut lockdown = LockdownClient::connect(&device.to_provider(UsbmuxdAddr::default(), CONNECTION_LABEL)).await?; + let values = lockdown.get_value(None, None).await?; + Ok(get_dict_string!(values, "DeviceName")) + } } impl fmt::Display for Device { @@ -38,22 +62,3 @@ impl fmt::Display for Device { ) } } - -macro_rules! get_dict_string { - ($dict:expr, $key:expr) => { - $dict - .as_dictionary() - .and_then(|dict| dict.get($key)) - .and_then(|v| v.as_string()) - .map(|s| s.to_string()) - .unwrap_or_else(|| "".to_string()) - }; -} - -async fn get_name_from_usbmuxd_device( - device: &UsbmuxdDevice, -) -> Result { - let mut lockdown = LockdownClient::connect(&device.to_provider(UsbmuxdAddr::default(), CONNECTION_LABEL)).await?; - let values = lockdown.get_value(None, None).await?; - Ok(get_dict_string!(values, "DeviceName")) -} diff --git a/apps/plumeimpactor/src/utils/mod.rs b/apps/plumeimpactor/src/utils/mod.rs new file mode 100644 index 00000000..70f74914 --- /dev/null +++ b/apps/plumeimpactor/src/utils/mod.rs @@ -0,0 +1,8 @@ +mod device; +mod package; + +pub use device::Device; +pub use package::Package; + +// TODO: make utils a shared package between the CLI and GUI apps +// or combine the GUI and CLI? like checkra1n maybe.. --gui --cli diff --git a/crates/types/src/package.rs b/apps/plumeimpactor/src/utils/package.rs similarity index 72% rename from crates/types/src/package.rs rename to apps/plumeimpactor/src/utils/package.rs index 3c0ec6b5..b24b1222 100644 --- a/crates/types/src/package.rs +++ b/apps/plumeimpactor/src/utils/package.rs @@ -7,9 +7,9 @@ use plist::Dictionary; use uuid::Uuid; use zip::ZipArchive; -use crate::bundle::Bundle; -use crate::PlistInfoTrait; -use errors::Error; +use grand_slam::Bundle; +use grand_slam::utils::{PlistInfoTrait, SignerSettings}; +use crate::Error; #[derive(Debug, Clone)] pub struct Package { @@ -45,7 +45,7 @@ impl Package { .filter_map(Result::ok) .map(|e| e.path()) .find(|p| p.is_dir() && p.extension().and_then(|e| e.to_str()) == Some("app")) - .ok_or_else(|| Error::BundleInfoPlistMissing)?; + .ok_or_else(|| Error::PackageInfoPlistMissing)?; Ok(Bundle::new(app_dir)?) } @@ -58,7 +58,7 @@ impl Package { .find(|name| name.starts_with("Payload/") && name.ends_with(".app/Info.plist") && name.matches('/').count() == 2) - .ok_or(Error::BundleInfoPlistMissing)? + .ok_or(Error::PackageInfoPlistMissing)? .to_string() }; let mut entry = archive.by_name(&info_name)?; @@ -66,6 +66,10 @@ impl Package { entry.read_to_end(&mut buf)?; Ok(plist::from_bytes(&buf)?) } + + pub fn remove_package_stage(self) { + fs::remove_dir_all(&self.stage_dir).ok(); + } } macro_rules! get_plist_dict_value { @@ -101,8 +105,26 @@ impl PlistInfoTrait for Package { } } -impl Drop for Package { - fn drop(&mut self) { - fs::remove_dir_all(&self.stage_dir).ok(); +impl Package { + // TODO: custom per-app settings + pub fn load_into_signer_settings<'settings, 'slf: 'settings>( + &'slf self, + settings: &'settings mut SignerSettings, + ) { + if let Some(identifier) = self.get_bundle_identifier() { + match identifier.as_str() { + "com.kdt.livecontainer" => { + settings.should_only_use_main_provisioning = true; + } + "com.SideStore.SideStore" => { + settings.should_embed_p12 = true; + } + _ => {} + } + } } + // depending on the apps pairing file placement, theres going to be different paths... + // Feather, StikDebug, Protokolle, Antrag, use ./pairingFile.plist + // LiveContainers uses ./SideStore/Documents/ALTPairingFile.mobiledevicepairing + // SideStore uses ./ALTPairingFile.mobiledevicepairing } diff --git a/apps/plumesign/Cargo.toml b/apps/plumesign/Cargo.toml index d937973e..1736b636 100644 --- a/apps/plumesign/Cargo.toml +++ b/apps/plumesign/Cargo.toml @@ -8,9 +8,8 @@ repository.workspace = true [dependencies] plist.workspace = true -ldid2 = { path = "../../crates/ldid2" } +tokio.workspace = true grand_slam = { path = "../../crates/grand_slam", features = ["vendored-botan"] } -types = { path = "../../crates/types"} +rustls = { version = "0.23.32", features = ["ring"] } clap = { version = "4.5", features = ["derive"] } -openssl = { version = "0.10", features = ["vendored"] } diff --git a/apps/plumesign/src/main.rs b/apps/plumesign/src/main.rs index 68a37f13..f119262f 100644 --- a/apps/plumesign/src/main.rs +++ b/apps/plumesign/src/main.rs @@ -3,69 +3,128 @@ use std::process::exit; use clap::Parser; -use ldid2::certificate::Certificate; -use ldid2::signing::signer::Signer; -use ldid2::signing::signer_settings::SignerSettings; -use types::Bundle; +use clap::{Args, Subcommand}; +use grand_slam::{CertificateIdentity, Bundle, MobileProvision, Signer}; +use grand_slam::utils::{PlistInfoTrait, SignerSettings}; #[derive(Debug, Parser)] #[command(author, version, about, disable_help_subcommand = true)] pub struct Cli { - // #[arg(short = 'w', help = "Shallow (sign only the top-level bundle)")] - // shallow: bool, - // #[arg(long = "pem", value_name = "PEM", num_args = 1.., help = "Paths to PEM files")] - // pem_files: Vec, - // #[arg(value_name = "BUNDLE", required = true, value_parser = clap::value_parser!(PathBuf), help = "Path to bundle to sign")] - // bundle: PathBuf, + #[command(subcommand)] + pub command: Commands, } -fn main() { +#[derive(Debug, Subcommand)] +pub enum Commands { + Sign(SignArgs), +} + +#[derive(Debug, Args)] +pub struct SignArgs { + #[arg(long = "pem", value_name = "PEM", num_args = 1.., required = true, help = "PEM files for certificate and private key")] + pub pem_files: Vec, + + #[arg(long = "provision", value_name = "PROVISION", num_args = 1.., required = true, help = "Provisioning profile files to embed")] + pub provisioning_files: Vec, + + #[arg(value_name = "BUNDLE", long = "bundle", required = true, help = "Path to the app bundle to sign")] + pub bundle: PathBuf, + + #[arg(long = "custom-identifier", value_name = "BUNDLE_ID", help = "Custom bundle identifier to set")] + pub bundle_identifier: Option, + + #[arg(long = "custom-name", value_name = "NAME", help = "Custom bundle name to set")] + pub name: Option, + + #[arg(long = "custom-version", value_name = "VERSION", help = "Custom bundle version to set")] + pub version: Option, + + // TODO: add support for p12, but for that to happen we need to patch + // the P12 crate to support SHA256 hashes... +} + +#[tokio::main] +async fn main() { let cli = Cli::parse(); - // if cli.pem_files.len() < 2 { - // eprintln!("Please provide at least two PEM files (certificate and key) using --pem."); - // exit(1); - // } - - // let signing_key = match Certificate::new(cli.pem_files.clone().into()) { - // Ok(cert) => cert, - // Err(e) => { - // eprintln!("Failed to create Certificate: {}", e); - // exit(1); - // } - // }; - - // let mut signer_settings = SignerSettings::default(); - // signer_settings.sign_shallow = cli.shallow; - - // let signer = Signer::new(Some(signing_key), signer_settings); - // if let Err(e) = signer.sign(vec![cli.bundle.clone()]) { - // eprintln!("Failed to sign bundle {:?}: {}", cli.bundle, e); - // exit(1); - // } - - // println!("{:?}", cli.bundle); - - // let bundle = Bundle::new(cli.bundle.clone()); - // match bundle { - // Ok(b) => { - // match b.get_embedded_bundles() { - // Ok(embedded_bundles) if !embedded_bundles.is_empty() => { - // for embedded_bundle in embedded_bundles { - // println!("{:?}", embedded_bundle.get_dir()); - // } - // } - // _ => { - // println!("No embedded bundles found."); - // } - // } - // } - // Err(e) => { - // eprintln!("Failed to open bundle {:?}: {}", cli.bundle, e); - // exit(1); - // } - // } - - - + rustls::crypto::ring::default_provider() + .install_default() + .expect("--x failed to install rustls crypto provider"); + + match &cli.command { + Commands::Sign(args) => { + if args.pem_files.len() < 2 { + eprintln!("--x at least two PEM files (certificate and key) are required via --pem."); + exit(1); + } + + let signing_key = CertificateIdentity::new_with_paths(args.pem_files.clone().into()).await.unwrap_or_else(|e| { + eprintln!("--x failed to create Certificate: {e}"); + exit(1); + }); + + let provisioning_files = args.provisioning_files.iter() + .map(MobileProvision::load_with_path) + .collect::, _>>() + .unwrap_or_else(|e| { + eprintln!("--x failed to load provisioning profiles: {e}"); + exit(1); + }); + + let signer_settings = SignerSettings { + custom_name: args.name.clone(), + custom_identifier: args.bundle_identifier.clone(), + custom_version: args.version.clone(), + ..Default::default() + }; + + let bundle = Bundle::new(args.bundle.clone()).unwrap_or_else(|e| { + eprintln!("--x failed to load bundle: {e}"); + exit(1); + }); + + if let Some(new_name) = signer_settings.custom_name.as_ref() { + if let Err(e) = bundle.set_name(new_name) { + eprintln!("--x Failed to set new name: {}", e); + exit(1); + } + } + + if let Some(new_version) = signer_settings.custom_version.as_ref() { + if let Err(e) = bundle.set_version(new_version) { + eprintln!("--x Failed to set new version: {}", e); + exit(1); + } + } + + if let Some(new_identifier) = &signer_settings.custom_identifier { + let original_identifier = bundle.get_bundle_identifier().unwrap(); + + match bundle.collect_bundles_sorted() { + Ok(bundles) => { + for b in bundles { + if let Err(e) = b.set_matching_identifier(&original_identifier, new_identifier) { + eprintln!("--x Failed to set new identifier: {}", e); + exit(1); + } + } + } + Err(e) => { + eprintln!("--x Failed to collect bundles: {}", e); + exit(1); + } + } + } + + let signer = Signer::new(Some(signing_key), signer_settings, provisioning_files); + + let target_path = args.bundle.clone(); + if let Err(e) = signer.sign_path(target_path.clone()) { + eprintln!("--x failed to sign: {e}"); + exit(1); + } + + println!("--> signed: {:?}", target_path); + } + } } diff --git a/crates/errors/Cargo.toml b/crates/errors/Cargo.toml deleted file mode 100644 index b60e9e80..00000000 --- a/crates/errors/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "errors" -edition.workspace = true -version.workspace = true -authors.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -idevice.workspace = true -plist.workspace = true -thiserror.workspace = true -zip.workspace = true -pem.workspace = true -x509-certificate.workspace = true -apple-codesign.workspace = true -reqwest.workspace = true -omnisette.workspace = true -serde_json.workspace = true diff --git a/crates/errors/src/lib.rs b/crates/errors/src/lib.rs deleted file mode 100644 index 4e2f2a04..00000000 --- a/crates/errors/src/lib.rs +++ /dev/null @@ -1,55 +0,0 @@ -use thiserror::Error as ThisError; - -#[derive(Debug, ThisError)] -pub enum Error { - #[error("Info.plist not found")] - BundleInfoPlistMissing, - #[error("Unknown bundle type")] - BundleTypeUnknown, - - #[error("Entitlements not found")] - ProvisioningEntitlementsUnknown, - - #[error("Developer session error {0}: {1}")] - DeveloperSession(i64, String), - #[error("Request to developer session failed")] - DeveloperSessionRequestFailed, - - #[error("Authentication SRP error {0}: {1}")] - AuthSrpWithMessage(i64, String), - #[error("Authentication SRP error")] - AuthSrp, - #[error("Authentication extra step required: {0}")] - ExtraStep(String), - #[error("Bad 2FA code")] - Bad2faCode, - #[error("Failed to parse")] - Parse, - - #[error("I/O error: {0}")] - Io(#[from] std::io::Error), - #[error("Plist error: {0}")] - Plist(#[from] plist::Error), - #[error("Zip error: {0}")] - Zip(#[from] zip::result::ZipError), - #[error("Codesign error: {0}")] - Codesign(#[from] apple_codesign::AppleCodesignError), - #[error("Certificate PEM error: {0}")] - Pem(#[from] pem::PemError), - #[error("X509 certificate error: {0}")] - X509(#[from] x509_certificate::X509CertificateError), - #[error("Idevice error: {0}")] - Idevice(#[from] idevice::IdeviceError), - #[error("Reqwest error: {0}")] - Reqwest(#[from] reqwest::Error), - #[error("Anisette error: {0}")] - Anisette(#[from] omnisette::AnisetteError), - #[error("Serde JSON error: {0}")] - SerdeJson(#[from] serde_json::Error), - - #[error("Missing certificate PEM data")] - CertificatePemMissing, - - #[error("Device not found")] - DeviceNotFound, -} diff --git a/crates/grand_slam/Cargo.toml b/crates/grand_slam/Cargo.toml index 9197052b..05d77c42 100644 --- a/crates/grand_slam/Cargo.toml +++ b/crates/grand_slam/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "grand_slam" +name = "grand_slam" # TODO: rename to sinners_road edition.workspace = true version.workspace = true authors.workspace = true -license.workspace = true +license = "MPL-2.0" repository.workspace = true [package.metadata.patch] @@ -16,7 +16,10 @@ uuid.workspace = true reqwest.workspace = true omnisette.workspace = true serde_json.workspace = true -errors = { path = "../errors" } +pem.workspace = true +x509-certificate.workspace = true +apple-codesign.workspace = true +thiserror.workspace = true rustls = { version = "0.23.32", features = ["ring"] } serde = { version = "1", features = ["derive"] } @@ -24,13 +27,21 @@ serde = { version = "1", features = ["derive"] } cbc = { version = "0.1.2", features = ["std"] } hmac = "0.12.1" pbkdf2 = "0.11" -sha2 = "0.10" -rand = "0.9" +sha2 = "0.10.9" +rand = "0.8.5" # cert needs this version instead of 0.9 apparently srp = "0.6.0" aes = "0.8.2" botan = "0.12.0" base64 = "0.22" +# certs +sha1 = "0.10.6" +rcgen = "0.9.3" +pem-rfc7468 = "0.7.0" +rsa = "0.9.8" +hex = "0.4.3" +p12 = "0.6.3" + [features] default = [] vendored-botan = ["botan/vendored"] diff --git a/crates/grand_slam/src/auth/account/login.rs b/crates/grand_slam/src/auth/account/login.rs index 3b7dbc77..15f9fd0c 100644 --- a/crates/grand_slam/src/auth/account/login.rs +++ b/crates/grand_slam/src/auth/account/login.rs @@ -5,7 +5,7 @@ use sha2::{Digest, Sha256}; use srp::client::{SrpClient, SrpClientVerifier}; use srp::groups::G_2048; -use errors::Error; +use crate::Error; use crate::auth::account::{check_error, parse_response}; use crate::auth::anisette_data::AnisetteData; diff --git a/crates/grand_slam/src/auth/account/mod.rs b/crates/grand_slam/src/auth/account/mod.rs index 44159443..2647be99 100644 --- a/crates/grand_slam/src/auth/account/mod.rs +++ b/crates/grand_slam/src/auth/account/mod.rs @@ -9,7 +9,7 @@ use reqwest::Response; use sha2::Sha256; use srp::client::SrpClientVerifier; -use errors::Error; +use crate::Error; pub async fn parse_response( res: Result, diff --git a/crates/grand_slam/src/auth/account/request.rs b/crates/grand_slam/src/auth/account/request.rs index 5d87d00c..f9934c0b 100644 --- a/crates/grand_slam/src/auth/account/request.rs +++ b/crates/grand_slam/src/auth/account/request.rs @@ -2,7 +2,7 @@ use plist::Dictionary; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use serde_json::Value; -use errors::Error; +use crate::Error; use crate::{SessionRequestTrait, auth::Account}; diff --git a/crates/grand_slam/src/auth/account/token.rs b/crates/grand_slam/src/auth/account/token.rs index 7ef9b5dd..635bd1d0 100644 --- a/crates/grand_slam/src/auth/account/token.rs +++ b/crates/grand_slam/src/auth/account/token.rs @@ -2,7 +2,7 @@ use botan::Cipher; use hmac::{Hmac, Mac}; use reqwest::header::{HeaderMap, HeaderValue}; -use errors::Error; +use crate::Error; use sha2::Sha256; use crate::auth::{Account, AppToken, AuthTokenRequest, AuthTokenRequestBody, GSA_ENDPOINT, RequestHeader}; diff --git a/crates/grand_slam/src/auth/account/two_factor_auth.rs b/crates/grand_slam/src/auth/account/two_factor_auth.rs index 2b92d5b0..f4491e4a 100644 --- a/crates/grand_slam/src/auth/account/two_factor_auth.rs +++ b/crates/grand_slam/src/auth/account/two_factor_auth.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use base64::{Engine, engine::general_purpose}; -use errors::Error; +use crate::Error; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use crate::auth::{Account, AuthenticationExtras, LoginState, PhoneNumber, VerifyBody, VerifyCode}; diff --git a/crates/grand_slam/src/auth/anisette_data.rs b/crates/grand_slam/src/auth/anisette_data.rs index 44add79a..fb74a59e 100644 --- a/crates/grand_slam/src/auth/anisette_data.rs +++ b/crates/grand_slam/src/auth/anisette_data.rs @@ -3,7 +3,7 @@ use std::time::SystemTime; use omnisette::{AnisetteConfiguration, AnisetteHeaders}; -use errors::Error; +use crate::Error; #[derive(Debug, Clone)] pub struct AnisetteData { diff --git a/crates/grand_slam/src/auth/mod.rs b/crates/grand_slam/src/auth/mod.rs index f387b641..551f45bd 100644 --- a/crates/grand_slam/src/auth/mod.rs +++ b/crates/grand_slam/src/auth/mod.rs @@ -5,17 +5,18 @@ use serde::{Deserialize, Serialize}; use omnisette::AnisetteConfiguration; use reqwest::{Certificate, Client, ClientBuilder}; use tokio::sync::Mutex; +use std::sync::Arc; -use errors::Error; +use crate::Error; use crate::auth::anisette_data::AnisetteData; const GSA_ENDPOINT: &str = "https://gsa.apple.com/grandslam/GsService2"; const APPLE_ROOT: &[u8] = include_bytes!("./apple_root.der"); +#[derive(Debug, Clone)] pub struct Account { - //TODO: move this to omnisette - pub anisette: Mutex, + pub anisette: Arc>, // pub spd: Option, //mutable spd pub spd: Option, @@ -36,9 +37,8 @@ impl Account { .http1_title_case_headers() .connection_verbose(true) .build()?; - Ok(Account { - anisette: Mutex::new(anisette), + anisette: Arc::new(Mutex::new(anisette)), spd: None, client, }) diff --git a/crates/grand_slam/src/developer/mod.rs b/crates/grand_slam/src/developer/mod.rs index d6c92184..9bb7cdc9 100644 --- a/crates/grand_slam/src/developer/mod.rs +++ b/crates/grand_slam/src/developer/mod.rs @@ -4,7 +4,7 @@ pub mod v1; use plist::{Dictionary, Value}; use uuid::Uuid; -use errors::Error; +use crate::Error; use crate::SessionRequestTrait; use crate::auth::{Account, account::request::RequestType}; @@ -58,7 +58,7 @@ impl SessionRequestTrait for DeveloperSession { let code = response_data.result_code.as_signed().unwrap_or(0); return Err(Error::DeveloperSession(code, msg.to_string())); } - + Ok(response) } diff --git a/crates/grand_slam/src/developer/qh/account.rs b/crates/grand_slam/src/developer/qh/account.rs index 137730da..bdd78d47 100644 --- a/crates/grand_slam/src/developer/qh/account.rs +++ b/crates/grand_slam/src/developer/qh/account.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use plist::{Dictionary, Value}; -use errors::Error; +use crate::Error; use crate::{SessionRequestTrait, developer_endpoint}; use super::{DeveloperSession, ResponseMeta}; diff --git a/crates/grand_slam/src/developer/qh/app_groups.rs b/crates/grand_slam/src/developer/qh/app_groups.rs index 6b55a00f..8a8a5b39 100644 --- a/crates/grand_slam/src/developer/qh/app_groups.rs +++ b/crates/grand_slam/src/developer/qh/app_groups.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use plist::{Dictionary, Value}; -use errors::Error; +use crate::Error; use crate::{SessionRequestTrait, developer_endpoint}; use super::{DeveloperSession, ResponseMeta}; @@ -32,8 +32,26 @@ impl DeveloperSession { Ok(response_data) } + + pub async fn qh_get_app_group(&self, team_id: &str, app_group_identifier: &str) -> Result, Error> { + let response_data = self.qh_list_app_groups(team_id).await?; + + let app_group = response_data.application_group_list.into_iter() + .find(|group| group.identifier == app_group_identifier); + + Ok(app_group) + } + + pub async fn qh_ensure_app_group(&self, team_id: &str, name: &str, identifier: &str) -> Result { + if let Some(app_group) = self.qh_get_app_group(team_id, identifier).await? { + Ok(app_group) + } else { + let response = self.qh_add_app_group(team_id, name, identifier).await?; + Ok(response.application_group) + } + } - pub async fn qh_assign_app_group(&self, team_id: &str, app_id_id: &str, app_group_id: &str) -> Result { + pub async fn qh_assign_app_group(&self, team_id: &str, app_id_id: &str, app_group_id: &str) -> Result { let endpoint = developer_endpoint!("/QH65B2/ios/assignApplicationGroupToAppId.action"); let mut body = Dictionary::new(); @@ -42,7 +60,7 @@ impl DeveloperSession { body.insert("applicationGroups".to_string(), Value::String(app_group_id.to_string())); let response = self.qh_send_request(&endpoint, Some(body)).await?; - let response_data: AppGroupResponse = plist::from_value(&Value::Dictionary(response))?; + let response_data: ResponseMeta = plist::from_value(&Value::Dictionary(response))?; Ok(response_data) } @@ -70,9 +88,9 @@ pub struct AppGroupResponse { #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct ApplicationGroup { - pub application_group: String, + pub application_group: String, // this is the actual identifier pub name: String, pub status: String, prefix: String, - pub identifier: String, + pub identifier: String, // this is the group.identifier } diff --git a/crates/grand_slam/src/developer/qh/app_ids.rs b/crates/grand_slam/src/developer/qh/app_ids.rs index 1107bf93..ce457eee 100644 --- a/crates/grand_slam/src/developer/qh/app_ids.rs +++ b/crates/grand_slam/src/developer/qh/app_ids.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use plist::{Dictionary, Integer, Value}; -use errors::Error; +use crate::Error; use crate::{SessionRequestTrait, developer_endpoint}; use super::{DeveloperSession, ResponseMeta}; @@ -18,7 +18,7 @@ impl DeveloperSession { Ok(response_data) } - + pub async fn qh_add_app_id(&self, team_id: &str, name: &str, identifier: &str) -> Result { let endpoint = developer_endpoint!("/QH65B2/ios/addAppId.action"); @@ -26,7 +26,7 @@ impl DeveloperSession { body.insert("teamId".to_string(), Value::String(team_id.to_string())); body.insert("name".to_string(), Value::String(name.to_string())); body.insert("identifier".to_string(), Value::String(identifier.to_string())); - + let response = self.qh_send_request(&endpoint, Some(body)).await?; let response_data: AppIDResponse = plist::from_value(&Value::Dictionary(response))?; @@ -71,6 +71,15 @@ impl DeveloperSession { Ok(app_id) } + + pub async fn qh_ensure_app_id(&self, team_id: &str, name: &str, identifier: &String) -> Result { + if let Some(app_id) = self.qh_get_app_id(team_id, identifier).await? { + Ok(app_id) + } else { + let response = self.qh_add_app_id(team_id, name, identifier).await?; + Ok(response.app_id) + } + } } #[allow(dead_code)] @@ -95,20 +104,20 @@ pub struct AppIDResponse { #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct AppID { - app_id_id: String, + pub app_id_id: String, name: String, app_id_platform: String, prefix: String, - identifier: String, + pub identifier: String, is_wild_card: bool, is_duplicate: bool, features: Features, enabled_features: Option>, is_dev_push_enabled: bool, is_prod_push_enabled: bool, - associated_application_groups_count: Integer, - associated_cloud_containers_count: Integer, - associated_identifiers_count: Integer, + associated_application_groups_count: Option, + associated_cloud_containers_count: Option, + associated_identifiers_count: Option, } #[allow(dead_code)] diff --git a/crates/grand_slam/src/developer/qh/certs.rs b/crates/grand_slam/src/developer/qh/certs.rs index 4c89cdd4..068aef29 100644 --- a/crates/grand_slam/src/developer/qh/certs.rs +++ b/crates/grand_slam/src/developer/qh/certs.rs @@ -1,7 +1,8 @@ use serde::Deserialize; use plist::{Data, Date, Dictionary, Integer, Value}; +use uuid::Uuid; -use errors::Error; +use crate::Error; use crate::{SessionRequestTrait, developer_endpoint}; use super::{DeveloperSession, ResponseMeta}; @@ -32,22 +33,20 @@ impl DeveloperSession { Ok(response_data) } - // pub async fn qh_submit_cert_csr(&self, team_id: &str, csr_data: &[u8], machine_name: &str) -> Result { - // let endpoint = developer_endpoint!("/QH65B2/ios/submitDevelopmentCSR.action"); + pub async fn qh_submit_cert_csr(&self, team_id: &str, csr_data: String, machine_name: &str) -> Result { + let endpoint = developer_endpoint!("/QH65B2/ios/submitDevelopmentCSR.action"); - // let mut body = Dictionary::new(); - // body.insert("teamId".to_string(), Value::String(team_id.to_string())); - // body.insert("csrContent".to_string(), Value::Data(Data::from(csr_data.to_vec()))); - // body.insert("machineId".to_string(), Value::String(Uuid::new_v4().to_string().to_uppercase())); - // body.insert("machineName".to_string(), Value::String(machine_name.to_string())); + let mut body = Dictionary::new(); + body.insert("teamId".to_string(), Value::String(team_id.to_string())); + body.insert("csrContent".to_string(), Value::String(csr_data)); + body.insert("machineId".to_string(), Value::String(Uuid::new_v4().to_string().to_uppercase())); + body.insert("machineName".to_string(), Value::String(machine_name.to_string())); - // let response = self.send_request(&endpoint, Some(body)).await?; - // println!("{:#?}", response); - // // let response_data: Cert = plist::from_value(&Value::Dictionary(response))?; + let response = self.qh_send_request(&endpoint, Some(body)).await?; + let response_data: CsrResponse = plist::from_value(&Value::Dictionary(response))?; - // // Ok(response_data) - // todo!("Implement CSR submission") - // } + Ok(response_data) + } } #[allow(dead_code)] @@ -62,6 +61,15 @@ pub struct CertsResponse { #[allow(dead_code)] #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] +pub struct CsrResponse { + pub cert_request: Csr, + #[serde(flatten)] + pub meta: ResponseMeta, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] pub struct Cert { pub name: String, pub certificate_id: String, @@ -72,13 +80,40 @@ pub struct Cert { certificate_platform: Option, pub cert_type: Option, pub cert_content: Data, - machine_id: Option, - machine_name: Option, + pub machine_id: Option, + pub machine_name: Option, } #[allow(dead_code)] #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] +pub struct Csr { + cert_request_id: String, + name: String, + status_code: Integer, + status_string: String, + csr_platform: String, + date_requested_string: String, + date_requested: Date, + date_created: Date, + owner_type: String, + owner_name: String, + owner_id: String, + pub certificate_id: String, + certificate_status_code: Integer, + cert_request_status_code: Integer, + certificate_type_display_id: String, + pub serial_num: String, + serial_num_decimal: String, + type_string: String, + pub certificate_type: Option, + pub machine_id: Option, + pub machine_name: Option, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] pub struct CertType { certificate_type_display_id: String, pub name: String, diff --git a/crates/grand_slam/src/developer/qh/devices.rs b/crates/grand_slam/src/developer/qh/devices.rs index 4dff1890..c6a52957 100644 --- a/crates/grand_slam/src/developer/qh/devices.rs +++ b/crates/grand_slam/src/developer/qh/devices.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use plist::{Dictionary, Date, Value}; -use errors::Error; +use crate::Error; use crate::{SessionRequestTrait, developer_endpoint}; use super::{DeveloperSession, ResponseMeta}; @@ -41,6 +41,15 @@ impl DeveloperSession { Ok(device) } + + pub async fn qh_ensure_device(&self, team_id: &str, device_name: &str, device_udid: &str) -> Result { + if let Some(device) = self.qh_get_device(team_id, device_udid).await? { + Ok(device) + } else { + let response = self.qh_add_device(team_id, device_name, device_udid).await?; + Ok(response.device) + } + } } #[allow(dead_code)] diff --git a/crates/grand_slam/src/developer/qh/mod.rs b/crates/grand_slam/src/developer/qh/mod.rs index 84df09d3..ae1384b3 100644 --- a/crates/grand_slam/src/developer/qh/mod.rs +++ b/crates/grand_slam/src/developer/qh/mod.rs @@ -4,6 +4,7 @@ pub mod app_ids; pub mod certs; pub mod devices; pub mod teams; +pub mod profile; use serde::Deserialize; use plist::Integer; diff --git a/crates/grand_slam/src/developer/qh/profile.rs b/crates/grand_slam/src/developer/qh/profile.rs new file mode 100644 index 00000000..e527962a --- /dev/null +++ b/crates/grand_slam/src/developer/qh/profile.rs @@ -0,0 +1,55 @@ +use serde::Deserialize; +use plist::{Data, Date, Dictionary, Value}; + +use crate::Error; + +use crate::{SessionRequestTrait, developer_endpoint}; +use super::{DeveloperSession, ResponseMeta}; + +impl DeveloperSession { + pub async fn qh_get_profile(&self, team_id: &str, app_id_id: &str) -> Result { + let endpoint = developer_endpoint!("/QH65B2/ios/downloadTeamProvisioningProfile.action"); + + let mut body = Dictionary::new(); + body.insert("teamId".to_string(), Value::String(team_id.to_string())); + body.insert("appIdId".to_string(), Value::String(app_id_id.to_string())); + + let response = self.qh_send_request(&endpoint, Some(body)).await?; + let response_data: ProfilesResponse = plist::from_value(&Value::Dictionary(response))?; + + Ok(response_data) + } +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ProfilesResponse { + pub provisioning_profile: Profile, + #[serde(flatten)] + pub meta: ResponseMeta, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Profile { + provisioning_profile_id: String, + name: String, + status: String, + #[serde(rename = "type")] + _type: String, + distribution_method: String, + pro_pro_platorm: Option, + #[serde(rename = "UUID")] + uuid: String, + pub date_expire: Date, + managing_app: Option, + // app_id: AppID, + app_id_id: String, + pub encoded_profile: Data, + pub filename: String, + is_template_profile: bool, + is_team_profile: bool, + is_free_provisioning_profile: Option, +} diff --git a/crates/grand_slam/src/developer/qh/teams.rs b/crates/grand_slam/src/developer/qh/teams.rs index d610dc52..9eeb5ac7 100644 --- a/crates/grand_slam/src/developer/qh/teams.rs +++ b/crates/grand_slam/src/developer/qh/teams.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use plist::{Date, Integer, Value}; -use errors::Error; +use crate::Error; use crate::{SessionRequestTrait, developer_endpoint}; use super::{DeveloperSession, ResponseMeta}; @@ -34,7 +34,7 @@ pub struct Team { pub name: String, pub team_id: String, #[serde(rename = "type")] - pub team_type: String, + pub _type: String, team_agent: Option, memberships: Vec, current_team_member: TeamMember, diff --git a/crates/grand_slam/src/developer/v1/app_ids.rs b/crates/grand_slam/src/developer/v1/app_ids.rs index 818a8f4b..fdf0ff08 100644 --- a/crates/grand_slam/src/developer/v1/app_ids.rs +++ b/crates/grand_slam/src/developer/v1/app_ids.rs @@ -6,7 +6,7 @@ use crate::SessionRequestTrait; use crate::auth::account::request::RequestType; use crate::developer_endpoint; -use errors::Error; +use crate::Error; impl DeveloperSession { pub async fn v1_list_app_ids(&self, team: &str) -> Result { diff --git a/crates/grand_slam/src/developer/v1/capabilities.rs b/crates/grand_slam/src/developer/v1/capabilities.rs index 290e6789..4e10b363 100644 --- a/crates/grand_slam/src/developer/v1/capabilities.rs +++ b/crates/grand_slam/src/developer/v1/capabilities.rs @@ -6,7 +6,7 @@ use crate::SessionRequestTrait; use crate::auth::account::request::RequestType; use crate::developer_endpoint; -use errors::Error; +use crate::Error; impl DeveloperSession { pub async fn v1_list_capabilities(&self, team: &str) -> Result { diff --git a/crates/grand_slam/src/lib.rs b/crates/grand_slam/src/lib.rs index 020b686b..6aa808bd 100644 --- a/crates/grand_slam/src/lib.rs +++ b/crates/grand_slam/src/lib.rs @@ -1,16 +1,80 @@ pub mod auth; pub mod developer; +pub mod utils; use plist::Dictionary; use serde_json::Value; -use errors::Error; - use crate::auth::account::request::RequestType; pub use omnisette::AnisetteConfiguration; +pub use utils::MachO; +pub use utils::MobileProvision; +pub use utils::CertificateIdentity; +pub use utils::Signer; +pub use utils::Bundle; +pub use utils::BundleType; trait SessionRequestTrait { async fn qh_send_request(&self, endpoint: &str, payload: Option) -> Result; async fn v1_send_request(&self, url: &str, body: Option, request_type: Option) -> Result; } + +use thiserror::Error as ThisError; + +#[derive(Debug, ThisError)] +pub enum Error { + #[error("Info.plist not found")] + BundleInfoPlistMissing, + #[error("Executable not found")] + BundleExecutableMissing, + + #[error("Entitlements not found")] + ProvisioningEntitlementsUnknown, + + #[error("Missing certificate PEM data")] + CertificatePemMissing, + #[error("Certificate error: {0}")] + Certificate(String), + + #[error("Developer session error {0}: {1}")] + DeveloperSession(i64, String), + #[error("Request to developer session failed")] + DeveloperSessionRequestFailed, + + #[error("Authentication SRP error {0}: {1}")] + AuthSrpWithMessage(i64, String), + #[error("Authentication SRP error")] + AuthSrp, + #[error("Authentication extra step required: {0}")] + ExtraStep(String), + #[error("Bad 2FA code")] + Bad2faCode, + #[error("Failed to parse")] + Parse, + + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + #[error("Plist error: {0}")] + Plist(#[from] plist::Error), + #[error("Codesign error: {0}")] + Codesign(#[from] apple_codesign::AppleCodesignError), + #[error("Certificate PEM error: {0}")] + Pem(#[from] pem::PemError), + #[error("X509 certificate error: {0}")] + X509(#[from] x509_certificate::X509CertificateError), + #[error("Reqwest error: {0}")] + Reqwest(#[from] reqwest::Error), + #[error("Anisette error: {0}")] + Anisette(#[from] omnisette::AnisetteError), + #[error("Serde JSON error: {0}")] + SerdeJson(#[from] serde_json::Error), + #[error("RSA error: {0}")] + Rsa(#[from] rsa::Error), + #[error("PKCS1 RSA error: {0}")] + PKCS1(#[from] rsa::pkcs1::Error), + #[error("PKCS8 RSA error: {0}")] + PKCS8(#[from] rsa::pkcs8::Error), + #[error("RCGen error: {0}")] + RcGen(#[from] rcgen::RcgenError), +} diff --git a/crates/types/src/bundle.rs b/crates/grand_slam/src/utils/bundle.rs similarity index 72% rename from crates/types/src/bundle.rs rename to crates/grand_slam/src/utils/bundle.rs index 6ebe40fb..d15dba79 100644 --- a/crates/types/src/bundle.rs +++ b/crates/grand_slam/src/utils/bundle.rs @@ -3,11 +3,11 @@ use std::path::PathBuf; use plist::Value; -use crate::PlistInfoTrait; +use super::PlistInfoTrait; -use errors::Error; +use crate::Error; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum BundleType { App, AppExtension, @@ -29,7 +29,7 @@ impl BundleType { #[derive(Debug, Clone)] pub struct Bundle { dir: PathBuf, - _type: BundleType, + pub _type: BundleType, info_plist_file: PathBuf, } @@ -55,14 +55,23 @@ impl Bundle { }) } - pub fn get_dir(&self) -> &PathBuf { + pub fn dir(&self) -> &PathBuf { &self.dir } - pub fn get_embedded_bundles(&self) -> Result, Error> { + pub fn collect_nested_bundles(&self) -> Result, Error> { collect_embeded_bundles_from_dir(&self.dir) } + pub fn collect_bundles_sorted(&self) -> Result, Error> { + let mut bundles = self.collect_nested_bundles()?; + bundles.push(self.clone()); + bundles.sort_by_key(|b| b.dir().components().count()); + bundles.reverse(); + + Ok(bundles) + } + pub fn set_info_plist_key>( &self, key: &str, @@ -77,6 +86,17 @@ impl Bundle { Ok(()) } + // TODO: we need to support changing lproj infoplist strings so localized names change as well + pub fn set_name(&self, new_name: &str) -> Result<(), Error> { + self.set_info_plist_key("CFBundleDisplayName", new_name)?; + self.set_info_plist_key("CFBundleName", new_name) + } + + pub fn set_version(&self, new_version: &str) -> Result<(), Error> { + self.set_info_plist_key("CFBundleShortVersionString", new_version)?; + self.set_info_plist_key("CFBundleVersion", new_version) + } + pub fn set_bundle_identifier(&self, new_identifier: &str) -> Result<(), Error> { self.set_info_plist_key("CFBundleIdentifier", new_identifier) } @@ -161,32 +181,50 @@ impl PlistInfoTrait for Bundle { } } -fn is_bundle_dir(name: &str) -> bool { - if let Some((_, ext)) = name.rsplit_once('.') { - BundleType::from_extension(ext).is_some() - } else { - false +impl Bundle { + pub fn is_sidestore(&self) -> bool { + matches!(self.get_bundle_identifier().as_deref(), Some("com.SideStore.SideStore")) } } fn collect_embeded_bundles_from_dir(dir: &PathBuf) -> Result, Error> { let mut bundles = Vec::new(); + + fn is_bundle_dir(name: &str) -> bool { + if let Some((_, ext)) = name.rsplit_once('.') { + BundleType::from_extension(ext).is_some() + } else { + false + } + } for entry in fs::read_dir(dir)? { - let entry = entry.map_err(|e| Error::Io(e))?; + let entry = entry.map_err(Error::Io)?; let path = entry.path(); + if path.file_name() + .and_then(|n| n.to_str()) + .map_or(false, |n| n.ends_with(".storyboardc")) + { + continue; + } + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { if is_bundle_dir(name) { if let Ok(bundle) = Bundle::new(&path) { - if let BundleType::App = bundle._type { - bundles.push(bundle); - } else { - if let Ok(embedded) = bundle.get_embedded_bundles() { - bundles.push(bundle); + if bundle.info_plist_file.parent() + .and_then(|p| p.file_name()) + .and_then(|n| n.to_str()) + .map_or(false, |n| n.ends_with(".storyboardc")) + { + continue; + } + + bundles.push(bundle.clone()); + + if bundle._type != BundleType::App { + if let Ok(embedded) = bundle.collect_nested_bundles() { bundles.extend(embedded); - } else { - bundles.push(bundle); } } continue; diff --git a/crates/grand_slam/src/utils/certificate.rs b/crates/grand_slam/src/utils/certificate.rs new file mode 100644 index 00000000..625f17da --- /dev/null +++ b/crates/grand_slam/src/utils/certificate.rs @@ -0,0 +1,297 @@ +use std::{fs, path::PathBuf, vec}; + +use apple_codesign::{cryptography::{InMemoryPrivateKey, PrivateKey}, SigningSettings}; +// TODO: why do we have pem and pem_rfc7468 deps again? +use pem_rfc7468::{LineEnding, encode_string}; +use rand::rngs::OsRng; +use rcgen::{DnType, KeyPair, PKCS_RSA_SHA256}; +use rsa::{RsaPrivateKey, pkcs1::EncodeRsaPublicKey, pkcs8::{DecodePrivateKey, EncodePrivateKey}}; +use x509_certificate::{CapturedX509Certificate, X509Certificate}; + +use crate::{Error, developer::{DeveloperSession, qh::certs::Cert}}; + +pub struct CertificateIdentity { + pub cert: Option, + pub key: Option>, + pub machine_id: Option, + pub serial_number: Option, + pub p12_data: Option> +} + +impl CertificateIdentity { + // Use for cli context or if you actually store pems? why would you do that though + pub async fn new_with_paths(paths: Option>) -> Result { + let mut cert = Self { + cert: None, + key: None, + machine_id: None, + p12_data: None, + serial_number: None, + }; + + if let Some(paths) = paths { + for path in &paths { + let pem_data = fs::read(path)?; + cert.resolve_certificate_from_contents(pem_data)?; + } + } + + Ok(cert) + } + + pub async fn new_with_session( + session: &DeveloperSession, + config_path: PathBuf, + machine_name: Option, + team_id: &String, + ) -> Result { + let machine_name = machine_name.unwrap_or_else(|| "AltStore".to_string()); + + let key_path = Self::key_dir(config_path, &team_id)?.join("key.pem"); + + let mut cert = Self { + cert: None, + key: None, + machine_id: None, + p12_data: None, + serial_number: None, + }; + + // To same some unnecessary requests, we're going to list our certificates first here + // then pass them into the necessary functions that need it, if the functions absolutely + // need to request certificates (after submitting a CSR, for example), they can do so + let certs = session + .qh_list_certs(&team_id) + .await? + .certificates; + + // Only the key will be written to disk, certificate can just be gotten via the request + // request we've made, by trying to match our public key with the requests public key + let key_pair: [Vec; 2] = if key_path.exists() { + let key_string = fs::read_to_string(&key_path)?; + let priv_key = RsaPrivateKey::from_pkcs8_pem(&key_string)?; + + if let Some(cert) = cert.find_certificate(certs.clone(), &priv_key, &machine_name).await? { + let cert_pem = encode_string("CERTIFICATE", LineEnding::LF, cert.cert_content.as_ref()).unwrap(); + let key_pem = priv_key.to_pkcs8_pem(Default::default())?.to_string(); + + [cert_pem.into_bytes(), key_pem.into_bytes()] + } else { + let (cert, priv_key) = cert.request_new_certificate(session, team_id, &machine_name, certs).await?; + let cert_pem = encode_string("CERTIFICATE", LineEnding::LF, cert.cert_content.as_ref()).unwrap(); + let key_pem = priv_key.to_pkcs8_pem(Default::default())?.to_string(); + + fs::write(&key_path, &key_pem)?; + [cert_pem.into_bytes(), key_pem.into_bytes()] + } + } else { + let (cert, priv_key) = cert.request_new_certificate(session, team_id, &machine_name, certs).await?; + let cert_pem = encode_string("CERTIFICATE", LineEnding::LF, cert.cert_content.as_ref()).unwrap(); + let key_pem = priv_key.to_pkcs8_pem(Default::default())?.to_string(); + + fs::write(&key_path, &key_pem)?; + [cert_pem.into_bytes(), key_pem.into_bytes()] + }; + + // TODO: this may be horrendious + // if let Some(p12_data) = cert.create_pkcs12(&key_pair) { + // cert.p12_data = Some(p12_data); + // } + + for pem in key_pair { + cert.resolve_certificate_from_contents(pem)?; + } + + Ok(cert) + } + + // /keys/ + fn key_dir(path: PathBuf, team_id: &String) -> Result { + let dir = path.join("keys").join(team_id); + + fs::create_dir_all(&dir)?; + + Ok(dir) + } + + // fn set_machine_id(&mut self, machine_id: String) { + // println!("Setting machine id: {}", machine_id); + // self.machine_id = Some(machine_id); + // } + + // fn set_serial_number(&mut self, serial_number: String) { + // println!("Setting serial number: {}", serial_number); + // self.serial_number = Some(serial_number); + // } + + // TODO: cleanest p12 code of them all + // pub fn create_pkcs12(&self, data: &[Vec; 2]) -> Option> { + // let machine_id = self.machine_id.as_ref()?; + // let cert_der = pem::parse(&data[0]).ok()?.contents().to_vec(); + // let key_der = pem::parse(&data[1]).ok()?.contents().to_vec(); + + // let p12 = p12::PFX::new(&cert_der, &key_der, None, &machine_id, "PLUME")?; + // Some(p12.to_der()) + // } + + // applecodesign-rs needs our contents as strings to sign + fn resolve_certificate_from_contents(&mut self, contents: Vec) -> Result<(), Error> { + for pem in pem::parse_many(contents).map_err(Error::Pem)? { + match pem.tag() { + "CERTIFICATE" => { + println!("CERTIFICATE loaded!"); // TODO: REMOVE SOME DEBUG STATEMENTS IF THIS WORKS WONDERFULY + self.cert = Some(CapturedX509Certificate::from_der(pem.contents())?); + } + "PRIVATE KEY" => { + println!("PRIVATE KEY loaded!"); // TODO: REMOVE SOME DEBUG STATEMENTS IF THIS WORKS WONDERFULY + self.key = Some(Box::new(InMemoryPrivateKey::from_pkcs8_der(pem.contents())?)); + } + "RSA PRIVATE KEY" => { + println!("RSA PRIVATE KEY loaded!"); // TODO: REMOVE SOME DEBUG STATEMENTS IF THIS WORKS WONDERFULY + self.key = Some(Box::new(InMemoryPrivateKey::from_pkcs1_der(pem.contents())?)); + } + tag => println!("(unhandled PEM tag {}; ignoring)", tag), + } + } + + Ok(()) + } + + pub fn load_into_signing_settings<'settings, 'slf: 'settings>( + &'slf self, + settings: &'settings mut SigningSettings<'slf>, + ) -> Result<(), Error> { + let signing_cert = self.cert.clone().ok_or(Error::CertificatePemMissing)?; + let signing_key = self.key.as_ref().ok_or(Error::CertificatePemMissing)?; + + settings.set_signing_key(signing_key.as_key_info_signer(), signing_cert); + settings.chain_apple_certificates(); + + Ok(()) + } +} + +impl CertificateIdentity { + async fn find_certificate( + &mut self, + certs: Vec, + priv_key: &RsaPrivateKey, + machine_name: &str, + ) -> Result, Error> { + let pub_key_der_obj = priv_key + .to_public_key() + .to_pkcs1_der()? + .as_bytes() + .to_vec(); + + for cert in certs { + if cert.machine_name.as_deref() == Some(machine_name) { + let parsed_cert = X509Certificate::from_der(&cert.cert_content)?; + if pub_key_der_obj == parsed_cert.public_key_data().as_ref() { + // We need to save the machine_id for our P12 + // if let Some(ref machine_id) = cert.machine_id { + // self.set_machine_id(machine_id.clone()); + // } + + // self.set_serial_number(cert.serial_number.clone()); + + return Ok(Some(cert)); + } + } + } + + Ok(None) + } + + async fn request_new_certificate( + &mut self, + session: &DeveloperSession, + team_id: &String, + machine_name: &String, + certs: Vec, + ) -> Result<(Cert, RsaPrivateKey), Error> { + let priv_key = RsaPrivateKey::new(&mut OsRng, 2048)?; + let priv_key_der = priv_key.to_pkcs8_der()?; + let priv_key_pair = KeyPair::from_der(priv_key_der.as_bytes())?; + + let mut params = rcgen::CertificateParams::new(vec![]); + params.alg = &PKCS_RSA_SHA256; + params.key_pair = Some(priv_key_pair); + + let dn = &mut params.distinguished_name; + dn.push(DnType::CountryName, "US"); + dn.push(DnType::StateOrProvinceName, "STATE"); + dn.push(DnType::LocalityName, "LOCAL"); + dn.push(DnType::OrganizationName, "ORGNIZATION"); + dn.push(DnType::CommonName, "CN"); + + let cert_csr = rcgen::Certificate::from_params(params)? + .serialize_request_pem()?; + + let cert_serial_numbers = certs + .iter() + .map(|c| c.serial_number.clone()) + .collect::>(); + + // When we submit a CSR theres a high chance of it failing, at least + // on free developer accounts, we put it in a loop so whenever it does + // fail, we also look through all of our existing certificates through + // the api until we have a success on a single revokage, then we can + // successfully submit our csr, but if we just cannot at all, return + // an error + let cert_id = loop { + match session + .qh_submit_cert_csr( + &team_id, + cert_csr.clone(), + machine_name, + ).await { + Ok(id) => break id, + Err(e) => { + // 7460 is for too many certificates (I think) + if matches!(&e, Error::DeveloperSession(code, _) if *code == 7460) { + // Try to revoke certificates from the candidate list + let mut revoked_any = false; + for cid in &cert_serial_numbers { + if session + .qh_revoke_cert(&team_id, cid) + .await + .is_ok() + { + revoked_any = true; + } + } + + if revoked_any { + continue; + } else { + return Err(Error::Certificate( + "Too many certificates and failed to revoke any".into(), + )); + } + } + + return Err(e) + } + } + }.cert_request; + + // We need to save the machine_id for our P12 + // if let Some(ref machine_id) = cert_id.machine_id { + // self.set_machine_id(machine_id.clone()); + // } + + // self.set_serial_number(cert_id.serial_num.clone()); + + // We request again, and hope this has our new certificate + // ready.... if not then woops... thats too bad isnt it + let certs = session + .qh_list_certs(&team_id) + .await? + .certificates + .into_iter() + .find(|c| c.certificate_id == cert_id.certificate_id); + + Ok((certs.ok_or(Error::CertificatePemMissing)?, priv_key)) + } +} diff --git a/crates/grand_slam/src/utils/macho.rs b/crates/grand_slam/src/utils/macho.rs new file mode 100644 index 00000000..9d1d9799 --- /dev/null +++ b/crates/grand_slam/src/utils/macho.rs @@ -0,0 +1,74 @@ +use std::{collections::HashSet, fs}; +use std::path::Path; + +use apple_codesign::MachFile; +use plist::{Dictionary, Value}; + +use crate::{Error, developer::v1::capabilities::Capability}; + +/// Represents a Mach-O file and its entitlements. +pub struct MachO { + _macho_file: MachFile<'static>, + pub entitlements: Option, +} + +impl MachO { + pub fn new>(path: P) -> Result { + let macho_data = fs::read(path)?; + // Leak the data for 'static lifetime required by MachFile. + let macho_data = Box::leak(macho_data.into_boxed_slice()); + let macho_file = MachFile::parse(macho_data)?; + let entitlements = Self::extract_entitlements(&macho_file)?; + + Ok(MachO { + _macho_file: macho_file, + entitlements, + }) + } + + fn extract_entitlements(macho_file: &MachFile<'_>) -> Result, Error> { + let macho = macho_file.nth_macho(0)?; + + if let Some(embedded_sig) = macho.code_signature()? { + if let Ok(Some(slot)) = embedded_sig.entitlements() { + let value = Value::from_reader_xml(slot.to_string().as_bytes())?; + if let Value::Dictionary(dict) = value { + return Ok(Some(dict)); + } + } + } + + Ok(None) + } + + pub fn app_groups_for_entitlements(&self) -> Option> { + self.entitlements + .as_ref() + .and_then(|e| e.get("com.apple.security.application-groups")?.as_array()) + .map(|arr| arr.iter().filter_map(|v| v.as_string().map(|s| s.to_string())).collect()) + } + + pub fn capabilities_for_entitlements(&self, capabilities: &[Capability]) -> Option> { + let entitlements = self.entitlements.as_ref()?; + let ent_keys: HashSet<_> = entitlements.keys().collect(); + + let capabilities_to_enable: Vec = capabilities + .iter() + .filter_map(|cap| { + cap.attributes.entitlements.as_ref().and_then(|ent_list| { + if ent_list.iter().any(|e| ent_keys.contains(&e.profile_key)) { + Some(cap.id.clone()) + } else { + None + } + }) + }) + .collect(); + + if capabilities_to_enable.is_empty() { + None + } else { + Some(capabilities_to_enable) + } + } +} diff --git a/crates/grand_slam/src/utils/mod.rs b/crates/grand_slam/src/utils/mod.rs new file mode 100644 index 00000000..ea8c0f50 --- /dev/null +++ b/crates/grand_slam/src/utils/mod.rs @@ -0,0 +1,61 @@ +mod certificate; +mod provision; +mod macho; +mod signer; +mod bundle; + +pub use macho::MachO; +pub use provision::MobileProvision; +pub use certificate::CertificateIdentity; +pub use signer::Signer; +pub use bundle::Bundle; +pub use bundle::BundleType; + +#[derive(Clone, Debug)] +pub struct SignerSettings { + pub custom_name: Option, + pub custom_identifier: Option, + pub custom_version: Option, + + pub support_minimum_os_version: bool, + pub support_file_sharing: bool, + pub support_ipad_fullscreen: bool, + pub support_game_mode: bool, + pub support_pro_motion: bool, + pub should_embed_provisioning: bool, + pub should_embed_pairing: bool, + pub should_embed_p12: bool, + pub should_only_use_main_provisioning: bool, + pub remove_url_schemes: bool, + pub export_ipa: bool, +} + +impl Default for SignerSettings { + fn default() -> Self { + Self { + custom_name: None, + custom_identifier: None, + custom_version: None, + + support_minimum_os_version: false, + support_file_sharing: false, + support_ipad_fullscreen: false, + support_game_mode: false, + support_pro_motion: false, + should_embed_provisioning: true, + should_embed_pairing: false, + should_embed_p12: false, + should_only_use_main_provisioning: false, + remove_url_schemes: false, + export_ipa: false, + } + } +} + +pub trait PlistInfoTrait { + fn get_name(&self) -> Option; + fn get_executable(&self) -> Option; + fn get_bundle_identifier(&self) -> Option; + fn get_version(&self) -> Option; + fn get_build_version(&self) -> Option; +} diff --git a/crates/grand_slam/src/utils/provision.rs b/crates/grand_slam/src/utils/provision.rs new file mode 100644 index 00000000..ab51cd98 --- /dev/null +++ b/crates/grand_slam/src/utils/provision.rs @@ -0,0 +1,147 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use plist::{Dictionary, Value}; +use crate::Error; + +use super::MachO; + +#[derive(Clone)] +pub struct MobileProvision { + pub provision_data: Vec, + provisioning_plist: Value, + entitlements: Dictionary, +} + +impl MobileProvision { + pub fn load_with_path>(provision_path: P) -> Result { + let path = provision_path.as_ref(); + + if !path.exists() { + return Err(Error::ProvisioningEntitlementsUnknown); + } + + let provision_data = fs::read(path)?; + let provisioning_plist = Self::extract_plist_from_file(&provision_data)?; + let entitlements = Self::extract_entitlements(&provisioning_plist)?; + + Ok(Self { + provision_data, + provisioning_plist, + entitlements, + }) + } + + pub fn load_with_bytes(provision_data: Vec) -> Result { + let provisioning_plist = Self::extract_plist_from_file(&provision_data)?; + let entitlements = Self::extract_entitlements(&provisioning_plist)?; + + Ok(Self { + provision_data, + provisioning_plist, + entitlements, + }) + } + + pub fn entitlements(&self) -> &Dictionary { + &self.entitlements + } + + pub fn replace_wildcard_in_entitlements(&mut self, new_application_id: &str) { + for value in self.entitlements.values_mut() { + match value { + Value::String(s) => { + if s.contains('*') { + *s = s.replace('*', new_application_id); + } + } + Value::Array(arr) => { + for item in arr.iter_mut() { + if let Value::String(s) = item { + if s.contains('*') { + *s = s.replace('*', new_application_id); + } + } + } + } + _ => {} + } + } + } + + pub fn merge_entitlements(&mut self, binary_path: PathBuf) -> Result<(), Error> { + let macho = MachO::new(&binary_path)?; + let binary_entitlements = macho.entitlements.ok_or(Error::ProvisioningEntitlementsUnknown)?; + + if let Some(Value::Array(other_groups)) = binary_entitlements.get("keychain-access-groups") { + self.entitlements.insert("keychain-access-groups".to_string(), Value::Array(other_groups.clone())); + } + + let new_team_id = self.entitlements + .get("com.apple.developer.team-identifier") + .and_then(Value::as_string) + .map(|s| s.to_owned()); + let old_team_id = binary_entitlements + .get("com.apple.developer.team-identifier") + .and_then(Value::as_string) + .map(|s| s.to_owned()) + .or(Some("AAAAA11111".to_string())); + // TODO: ^^ not hardcode livecontainers placeholder team identifier + + if let (Some(new_id), Some(old_id)) = (new_team_id.as_ref(), old_team_id.as_ref()) { + if let Some(Value::Array(groups)) = self.entitlements.get_mut("keychain-access-groups") { + for group in groups.iter_mut() { + if let Value::String(s) = group { + if s.contains(old_id) { + *s = s.replace(old_id, new_id); + } + } + } + } + } + + Ok(()) + } + + pub fn entitlements_as_bytes(&self) -> Result, Error> { + let mut buf = Vec::new(); + Value::Dictionary(self.entitlements.clone()).to_writer_xml(&mut buf)?; + Ok(buf) + } + + pub fn bundle_id(&self) -> Option { + let app_id = self.entitlements.get("application-identifier")?.as_string()?; + + let prefix = self.provisioning_plist + .as_dictionary()? + .get("ApplicationIdentifierPrefix")? + .as_array()? + .get(0)? + .as_string(); + + if let Some(prefix) = prefix { + app_id.strip_prefix(prefix) + .map(|rest| rest.trim_start_matches('.').to_string()) + .or_else(|| Some(app_id.to_string())) + } else { + Some(app_id.to_string()) + } + } + + fn extract_plist_from_file(data: &[u8]) -> Result { + let start = data.windows(6).position(|w| w == b"").ok_or(Error::ProvisioningEntitlementsUnknown)? + 8; + let plist_data = &data[start..end]; + let plist = plist::Value::from_reader_xml(plist_data)?; + Ok(plist) + } + + fn extract_entitlements(plist: &Value) -> Result { + plist + .as_dictionary() + .and_then(|d| d.get("Entitlements")) + .and_then(|v| v.as_dictionary()) + .cloned() + .ok_or(Error::ProvisioningEntitlementsUnknown) + } +} diff --git a/crates/grand_slam/src/utils/signer.rs b/crates/grand_slam/src/utils/signer.rs new file mode 100644 index 00000000..47b357df --- /dev/null +++ b/crates/grand_slam/src/utils/signer.rs @@ -0,0 +1,100 @@ +use std::fs; +use std::path::PathBuf; + +use apple_codesign::{SigningSettings, UnifiedSigner}; + +use crate::Error; + +use super::{CertificateIdentity, MobileProvision}; +use super::SignerSettings; +use super::{Bundle, BundleType, PlistInfoTrait}; + +pub struct Signer { + certificate: Option, + settings: SignerSettings, + provisioning_files: Vec, +} + +impl Signer { + pub fn new( + certificate: Option, + settings: SignerSettings, + provisioning_files: Vec, + ) -> Self { + Self { + certificate, + settings, + provisioning_files, + } + } + + pub fn sign_path(&self, path: PathBuf) -> Result<(), Error> { + let bundle = Bundle::new(path)?; + self.sign_bundle(&bundle) + } + + pub fn sign_bundle(&self, bundle: &Bundle) -> Result<(), Error> { + let bundles = bundle.collect_bundles_sorted()?; + + for bundle in &bundles { + let mut settings = self.build_base_settings()?; + + if bundle._type == BundleType::AppExtension || bundle._type == BundleType::App { + let mut matched_prov = None; + + for prov in &self.provisioning_files { + if let (Some(bundle_id), Some(team_id)) = (bundle.get_bundle_identifier(), prov.bundle_id()) { + if team_id == bundle_id { + matched_prov = Some(prov); + break; + } + } + } + + let mut prov = matched_prov.unwrap_or_else(|| &self.provisioning_files[0]).clone(); + + if let Some(bundle_id) = bundle.get_bundle_identifier() { + prov.replace_wildcard_in_entitlements(&bundle_id); + } + + if let Some(bundle_executable) = bundle.get_executable() { + let binary_path = bundle.dir().join(bundle_executable); + prov.merge_entitlements(binary_path).ok(); // if it fails we can ignore + } + + if self.settings.should_embed_provisioning { + fs::write(bundle.dir().join("embedded.mobileprovision"), &prov.provision_data)?; + } + + if let Ok(ent_xml) = prov.entitlements_as_bytes() { + settings.set_entitlements_xml( + apple_codesign::SettingsScope::Main, + String::from_utf8_lossy(&ent_xml) + )?; + } + } + + UnifiedSigner::new(settings).sign_path_in_place(bundle.dir())?; + } + + if let Some(cert) = &self.certificate { + if let Some(key) = &cert.key { + key.finish()?; + } + } + + Ok(()) + } + + fn build_base_settings(&self) -> Result, Error> { + let mut settings = SigningSettings::default(); + if let Some(cert) = &self.certificate { + cert.load_into_signing_settings(&mut settings)?; + settings.set_team_id_from_signing_certificate(); + } + settings.set_for_notarization(false); + settings.set_shallow(false); + settings.set_team_id_from_signing_certificate(); + Ok(settings) + } +} diff --git a/crates/ldid2/Cargo.toml b/crates/ldid2/Cargo.toml deleted file mode 100644 index 841f17df..00000000 --- a/crates/ldid2/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "ldid2" # sinners_road -edition.workspace = true -version.workspace = true -authors.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -pem.workspace = true -x509-certificate.workspace = true -apple-codesign.workspace = true -plist.workspace = true -errors = { path = "../errors" } -types = { path = "../types" } diff --git a/crates/ldid2/src/certificate.rs b/crates/ldid2/src/certificate.rs deleted file mode 100644 index 46e5fce6..00000000 --- a/crates/ldid2/src/certificate.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::path::PathBuf; - -use apple_codesign::{cryptography::{InMemoryPrivateKey, PrivateKey}, SigningSettings}; -use x509_certificate::CapturedX509Certificate; - -use errors::Error; - -pub struct Certificate { - pub cert: Option, - pub key: Option>, -} - -impl Certificate { - pub fn new(paths: Option>) -> Result { - let mut cert = Self { - cert: None, - key: None - }; - - if let Some(paths) = paths { - for path in &paths { - cert.resolve_certificate_from_path(path)?; - } - } - - Ok(cert) - } - - pub fn resolve_certificate_from_path(&mut self, path: &PathBuf) -> Result<(), Error> { - println!("reading PEM data from {}", path.display()); - let pem_data = std::fs::read(path)?; - - for pem in pem::parse_many(pem_data).map_err(Error::Pem)? { - match pem.tag() { - "CERTIFICATE" => { - println!("adding certificate from {}", path.display()); - self.cert = Some(CapturedX509Certificate::from_der(pem.contents())?); - } - "PRIVATE KEY" => { - println!("adding private key from {}", path.display()); - self.key = Some(Box::new(InMemoryPrivateKey::from_pkcs8_der(pem.contents())?)); - } - "RSA PRIVATE KEY" => { - println!("adding RSA private key from {}", path.display()); - self.key = Some(Box::new(InMemoryPrivateKey::from_pkcs1_der(pem.contents())?)); - } - tag => println!("(unhandled PEM tag {}; ignoring)", tag), - } - } - - Ok(()) - } - - pub fn load_into_signing_settings<'settings, 'slf: 'settings>( - &'slf self, - settings: &'settings mut SigningSettings<'slf>, - ) -> Result<(), Error> { - let signing_cert = self.cert.clone().ok_or(Error::CertificatePemMissing)?; - let signing_key = self.key.as_ref().ok_or(Error::CertificatePemMissing)?; - - if !signing_cert.time_constraints_valid(None) { - println!( - "Warning: signing certificate expired as of {}; signatures may not be valid", - signing_cert.validity_not_after().to_rfc3339() - ); - } - - settings.set_signing_key(signing_key.as_key_info_signer(), signing_cert); - - if let Some(certs) = settings.chain_apple_certificates() { - for cert in certs { - println!( - "Automatically registered Apple CA certificate: {}", - cert.subject_common_name().unwrap_or_else(|| "default".into()) - ); - } - } - - Ok(()) - } -} diff --git a/crates/ldid2/src/lib.rs b/crates/ldid2/src/lib.rs deleted file mode 100644 index 9a229df4..00000000 --- a/crates/ldid2/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod certificate; -pub mod signing; -pub mod macho; -pub mod provision; diff --git a/crates/ldid2/src/macho.rs b/crates/ldid2/src/macho.rs deleted file mode 100644 index 165a7bdc..00000000 --- a/crates/ldid2/src/macho.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::fs; -use std::path::PathBuf; - -use apple_codesign::MachFile; - -use errors::Error; - -pub struct MachO<'a> { - macho_file: MachFile<'a>, -} - -impl<'a> MachO<'a> { - pub fn new(path: impl Into) -> Result { - let path = path.into(); - let macho_data = fs::read(&path)?; - let macho_data = Box::leak(macho_data.into_boxed_slice()); - let macho_file = MachFile::parse(macho_data)?; - - Ok(MachO { - macho_file, - }) - } - - pub fn get_entitlements(&self) -> Result, Error> { - let macho = self.macho_file.nth_macho(0)?; - if let Some(embedded_sig) = macho.code_signature()? { - if let Ok(Some(slot)) = embedded_sig.entitlements() { - return Ok(Some(slot.to_string())); - } - } - Ok(None) - } -} diff --git a/crates/ldid2/src/provision.rs b/crates/ldid2/src/provision.rs deleted file mode 100644 index d97f8e6b..00000000 --- a/crates/ldid2/src/provision.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::fs; -use std::path::PathBuf; - -use plist::{Dictionary, Value}; - -use errors::Error; - -pub struct MobileProvision { - provision_file: PathBuf, - provision_entitlements_dictionary: Value, -} - -impl MobileProvision { - pub fn new>(provision_path: P) -> Result { - let path = provision_path.into(); - - if !path.exists() { - return Err(Error::ProvisioningEntitlementsUnknown); - } - - let provision_entitlements_dictionary = Self::extract_entitlements_from_provision_file(&path)?; - - Ok(Self { - provision_file: path.clone(), - provision_entitlements_dictionary, - }) - } - - pub fn get_file_path(&self) -> &PathBuf { - &self.provision_file - } - - fn extract_entitlements_from_provision_file(provision_file: &PathBuf) -> Result { - let data = fs::read(provision_file)?; - let start = data.windows(6).position(|w| w == b"").ok_or(Error::ProvisioningEntitlementsUnknown)? + 8; - let plist_data = &data[start..end]; - - let plist = plist::Value::from_reader_xml(plist_data)?; - let dict = plist - .as_dictionary() - .and_then(|d| d.get("Entitlements")) - .and_then(|v| v.as_dictionary()) - .cloned() - .ok_or(Error::ProvisioningEntitlementsUnknown)?; - - Ok(Value::Dictionary(dict)) - } - - pub fn get_entitlements_as_bytes(&self) -> Result, Error> { - let mut buf = Vec::new(); - self.provision_entitlements_dictionary.to_writer_xml(&mut buf)?; - Ok(buf) - } - - pub fn get_entitlements_dictionary(&self) -> Result<&Dictionary, Error> { - self.provision_entitlements_dictionary.as_dictionary().ok_or(Error::ProvisioningEntitlementsUnknown) - } - -} diff --git a/crates/ldid2/src/signing/mod.rs b/crates/ldid2/src/signing/mod.rs deleted file mode 100644 index 9fc1d765..00000000 --- a/crates/ldid2/src/signing/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod signer_settings; -pub mod signer; diff --git a/crates/ldid2/src/signing/signer.rs b/crates/ldid2/src/signing/signer.rs deleted file mode 100644 index cf9071f4..00000000 --- a/crates/ldid2/src/signing/signer.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::fs; -use std::path::PathBuf; - -use apple_codesign::{SigningSettings, UnifiedSigner}; - -use errors::Error; -use crate::certificate::Certificate; -use super::signer_settings::SignerSettings; -use types::Bundle; - -pub struct Signer { - certificate: Option, - settings: SignerSettings, -} - -impl Signer { - pub fn new(certificate: Option, settings: SignerSettings) -> Self { - Signer { certificate, settings } - } - - pub fn sign(&self, paths: Vec) -> Result<(), Error> { - let mut settings = SigningSettings::default(); - - if let Some(certificate) = &self.certificate { - certificate.load_into_signing_settings(&mut settings)?; - } - - settings.set_team_id_from_signing_certificate(); - settings.set_shallow(self.settings.sign_shallow); - settings.set_for_notarization(false); - - let signer = UnifiedSigner::new(settings); - - - - // signer.sign_path_in_place(&paths)?; - - if let Some(certificate) = &self.certificate { - if let Some(key) = &certificate.key { - key.finish()?; - } - } - - Ok(()) - } -} diff --git a/crates/ldid2/src/signing/signer_settings.rs b/crates/ldid2/src/signing/signer_settings.rs deleted file mode 100644 index c76c6004..00000000 --- a/crates/ldid2/src/signing/signer_settings.rs +++ /dev/null @@ -1,39 +0,0 @@ - - - -// for app extensions, there should be three options -// - default (sign and add mobileprovisions to everything) -// - default-remove-plugins (just like default, but some extensions are removed) -// - zsign (sign extensions with the main apps mobileprovision) -pub enum SignerMode { - Default, - Zsign, -} - -pub struct SignerSettings { - pub sign_shallow: bool, - pub sign_mode: SignerMode, - pub custom_name: Option, - pub custom_identifier: Option, - pub custom_version: Option, - pub custom_build_version: Option, - pub support_file_sharing: Option, - pub support_older_versions: Option, - pub support_more_devices: Option, -} - -impl Default for SignerSettings { - fn default() -> Self { - Self { - sign_shallow: false, - sign_mode: SignerMode::Default, - custom_name: None, - custom_identifier: None, - custom_version: None, - custom_build_version: None, - support_file_sharing: None, - support_older_versions: None, - support_more_devices: None, - } - } -} diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml deleted file mode 100644 index 7b1cedfa..00000000 --- a/crates/types/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "types" -edition.workspace = true -version.workspace = true -authors.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -idevice.workspace = true -thiserror.workspace = true -plist.workspace = true -zip.workspace = true -tokio.workspace = true -uuid.workspace = true -errors = { path = "../errors" } diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs deleted file mode 100644 index 4a2fd551..00000000 --- a/crates/types/src/lib.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod bundle; -mod device; -mod package; - -pub use bundle::Bundle; -pub use device::Device; -pub use package::Package; - -pub trait PlistInfoTrait { - fn get_name(&self) -> Option; - fn get_executable(&self) -> Option; - fn get_bundle_identifier(&self) -> Option; - fn get_version(&self) -> Option; - fn get_build_version(&self) -> Option; -} diff --git a/package/linux/.keep b/package/linux/.keep new file mode 100644 index 00000000..e69de29b diff --git a/package/macos/Impactor.app/Contents/Info.plist b/package/macos/Impactor.app/Contents/Info.plist new file mode 100644 index 00000000..a63c4ecb --- /dev/null +++ b/package/macos/Impactor.app/Contents/Info.plist @@ -0,0 +1,34 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleDisplayName + plumeimpactor + CFBundleExecutable + plumeimpactor + CFBundleIdentifier + bucket.plumeimpactor + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + plumeimpactor + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.0.1 + CFBundleVersion + 1 + CSResourcesFileMapped + + LSRequiresCarbon + + NSHighResolutionCapable + + CFBundleIconFile + AppIcon + CFBundleIconName + AppIcon + + diff --git a/package/macos/Impactor.app/Contents/Resources/AppIcon.icns b/package/macos/Impactor.app/Contents/Resources/AppIcon.icns new file mode 100644 index 00000000..892464e4 Binary files /dev/null and b/package/macos/Impactor.app/Contents/Resources/AppIcon.icns differ diff --git a/package/macos/Impactor.app/Contents/Resources/Assets.car b/package/macos/Impactor.app/Contents/Resources/Assets.car new file mode 100644 index 00000000..40ef43ed Binary files /dev/null and b/package/macos/Impactor.app/Contents/Resources/Assets.car differ diff --git a/package/windows/.keep b/package/windows/.keep new file mode 100644 index 00000000..e69de29b