mirror of
https://github.com/nab138/isideload.git
synced 2026-03-02 14:36:16 +01:00
Start isideload-next
This commit is contained in:
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
@@ -15,8 +15,9 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- platform: "ubuntu-22.04"
|
- platform: "ubuntu-latest"
|
||||||
- platform: "windows-latest"
|
- platform: "windows-latest"
|
||||||
|
- platform: "macos-latest"
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
@@ -29,16 +30,12 @@ jobs:
|
|||||||
~/.cargo/registry
|
~/.cargo/registry
|
||||||
~/.cargo/git
|
~/.cargo/git
|
||||||
target
|
target
|
||||||
key: ${{ runner.os }}-tauri-${{ hashFiles('Cargo.lock') }}
|
key: ${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-tauri-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
- name: Add MSVC to PATH
|
|
||||||
if: matrix.platform == 'windows-latest'
|
|
||||||
uses: ilammy/msvc-dev-cmd@v1
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --features "vendored-openssl"
|
run: cargo build
|
||||||
|
|||||||
5643
Cargo.lock
generated
5643
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["isideload", "examples/minimal"]
|
members = ["isideload"]
|
||||||
default-members = ["isideload"]
|
default-members = ["isideload"]
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 nab138
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -96,13 +96,13 @@ See [examples/minimal/src/main.rs](examples/minimal/src/main.rs).
|
|||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
This project is licensed under the MPL-2.0 License. See the [LICENSE](LICENSE) file for details.
|
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- The amazing [idevice](https://github.com/jkcoxson/idevice) crate is used to communicate with the device
|
- The amazing [idevice](https://github.com/jkcoxson/idevice) crate is used to communicate with the device
|
||||||
|
|
||||||
- Packages from [`apple-private-apis`](https://github.com/SideStore/apple-private-apis) were used for authentication, but the original project was left unfinished. To support isideload, `apple-private-apis` was forked and modified to add missing features. With permission from the original developers, the fork was published to crates.io until the official project is published.
|
- Packages from [`apple-private-apis`](https://github.com/SideStore/apple-private-apis), which is licensed under MPL-2.0, were used for authentication, but the original project was left unfinished. To support isideload, `apple-private-apis` was [forked](https://github.com/nab138/apple-private-apis) and modified to add missing features. With permission from the original developers, the fork was published to crates.io until the official project is published.
|
||||||
|
|
||||||
- [apple-codesign](https://crates.io/crates/apple-codesign) was used for code signing, which is licensed under MPL-2.0.
|
- [apple-codesign](https://crates.io/crates/apple-codesign) was used for code signing, which is licensed under MPL-2.0.
|
||||||
|
|
||||||
|
|||||||
5
examples/minimal/.gitignore
vendored
5
examples/minimal/.gitignore
vendored
@@ -1,5 +0,0 @@
|
|||||||
.zsign_cache
|
|
||||||
keys
|
|
||||||
*.ipa
|
|
||||||
state.plist
|
|
||||||
*.mobileprovision
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "minimal"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
isideload = { path = "../../isideload" }
|
|
||||||
idevice = { version = "0.1.46", features = ["usbmuxd", "ring"], default-features = false}
|
|
||||||
tokio = { version = "1.43", features = ["macros", "rt-multi-thread"] }
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
use std::{env, path::PathBuf, sync::Arc};
|
|
||||||
|
|
||||||
use idevice::usbmuxd::{UsbmuxdAddr, UsbmuxdConnection};
|
|
||||||
use isideload::{
|
|
||||||
AnisetteConfiguration, AppleAccount, SideloadConfiguration,
|
|
||||||
developer_session::DeveloperSession, sideload::sideload_app,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let args: Vec<String> = env::args().collect();
|
|
||||||
let app_path = PathBuf::from(
|
|
||||||
args.get(1)
|
|
||||||
.expect("Please provide the path to the app to install"),
|
|
||||||
);
|
|
||||||
let apple_id = args
|
|
||||||
.get(2)
|
|
||||||
.expect("Please provide the Apple ID to use for installation");
|
|
||||||
let apple_password = args.get(3).expect("Please provide the Apple ID password");
|
|
||||||
|
|
||||||
// You don't have to use usbmuxd, you can use any IdeviceProvider
|
|
||||||
let usbmuxd = UsbmuxdConnection::default().await;
|
|
||||||
if usbmuxd.is_err() {
|
|
||||||
panic!("Failed to connect to usbmuxd: {:?}", usbmuxd.err());
|
|
||||||
}
|
|
||||||
let mut usbmuxd = usbmuxd.unwrap();
|
|
||||||
|
|
||||||
let devs = usbmuxd.get_devices().await.unwrap();
|
|
||||||
if devs.is_empty() {
|
|
||||||
panic!("No devices found");
|
|
||||||
}
|
|
||||||
|
|
||||||
let provider = devs
|
|
||||||
.iter()
|
|
||||||
.next()
|
|
||||||
.unwrap()
|
|
||||||
.to_provider(UsbmuxdAddr::from_env_var().unwrap(), "isideload-demo");
|
|
||||||
|
|
||||||
// Change the anisette url and such here
|
|
||||||
// Note that right now only remote anisette servers are supported
|
|
||||||
let anisette_config = AnisetteConfiguration::default();
|
|
||||||
|
|
||||||
let get_2fa_code = || {
|
|
||||||
let mut code = String::new();
|
|
||||||
println!("Enter 2FA code:");
|
|
||||||
std::io::stdin().read_line(&mut code).unwrap();
|
|
||||||
Ok(code.trim().to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
let account = AppleAccount::login(
|
|
||||||
|| Ok((apple_id.to_string(), apple_password.to_string())),
|
|
||||||
get_2fa_code,
|
|
||||||
anisette_config,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let dev_session = DeveloperSession::new(Arc::new(account));
|
|
||||||
|
|
||||||
// You can change the machine name, store directory (for certs, anisette data, & provision files), and logger
|
|
||||||
let config = SideloadConfiguration::default()
|
|
||||||
.set_machine_name("isideload-demo".to_string())
|
|
||||||
.set_force_sidestore(true);
|
|
||||||
|
|
||||||
sideload_app(&provider, &dev_session, app_path, config)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "isideload"
|
name = "isideload"
|
||||||
description = "Sideload iOS/iPadOS applications"
|
description = "Sideload iOS/iPadOS applications"
|
||||||
license = "MPL-2.0"
|
license = "MIT"
|
||||||
authors = ["Nicholas Sharp <nab@nabdev.me>"]
|
authors = ["Nicholas Sharp <nab@nabdev.me>"]
|
||||||
version = "0.1.21"
|
version = "0.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
repository = "https://github.com/nab138/isideload"
|
repository = "https://github.com/nab138/isideload"
|
||||||
documentation = "https://docs.rs/isideload"
|
documentation = "https://docs.rs/isideload"
|
||||||
@@ -14,19 +14,3 @@ readme = "../README.md"
|
|||||||
default = []
|
default = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
plist = { version = "1.7" }
|
|
||||||
icloud_auth = { version = "0.1.5", package = "nab138_icloud_auth" }
|
|
||||||
uuid = { version = "1.17.0", features = ["v4"] }
|
|
||||||
zip = { version = "4.3", default-features = false, features = ["deflate"] }
|
|
||||||
hex = "0.4"
|
|
||||||
sha1 = "0.10"
|
|
||||||
idevice = { version = "0.1.46", features = ["afc", "installation_proxy", "ring"], default-features = false }
|
|
||||||
thiserror = "2"
|
|
||||||
apple-codesign = "0.29.0"
|
|
||||||
x509-certificate = "0.24.0"
|
|
||||||
rsa = "0.9"
|
|
||||||
rcgen = "0.13"
|
|
||||||
rand = "0.8"
|
|
||||||
tokio = "1.48.0"
|
|
||||||
p12 = "0.6.3"
|
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
|
|
||||||
|
|
||||||
use crate::Error;
|
|
||||||
use crate::bundle::Bundle;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use zip::ZipArchive;
|
|
||||||
|
|
||||||
pub struct Application {
|
|
||||||
pub bundle: Bundle,
|
|
||||||
//pub temp_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Application {
|
|
||||||
pub fn new(path: PathBuf) -> Result<Self, Error> {
|
|
||||||
if !path.exists() {
|
|
||||||
return Err(Error::InvalidBundle(
|
|
||||||
"Application path does not exist".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut bundle_path = path.clone();
|
|
||||||
//let mut temp_path = PathBuf::new();
|
|
||||||
|
|
||||||
if path.is_file() {
|
|
||||||
let temp_dir = std::env::temp_dir();
|
|
||||||
let temp_path = temp_dir
|
|
||||||
.join(path.file_name().unwrap().to_string_lossy().to_string() + "_extracted");
|
|
||||||
if temp_path.exists() {
|
|
||||||
std::fs::remove_dir_all(&temp_path).map_err(Error::Filesystem)?;
|
|
||||||
}
|
|
||||||
std::fs::create_dir_all(&temp_path).map_err(Error::Filesystem)?;
|
|
||||||
|
|
||||||
let file = File::open(&path).map_err(Error::Filesystem)?;
|
|
||||||
let mut archive = ZipArchive::new(file).map_err(|e| {
|
|
||||||
Error::Generic(format!("Failed to open application archive: {}", e))
|
|
||||||
})?;
|
|
||||||
archive.extract(&temp_path).map_err(|e| {
|
|
||||||
Error::Generic(format!("Failed to extract application archive: {}", e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let payload_folder = temp_path.join("Payload");
|
|
||||||
if payload_folder.exists() && payload_folder.is_dir() {
|
|
||||||
let app_dirs: Vec<_> = std::fs::read_dir(&payload_folder)
|
|
||||||
.map_err(|e| {
|
|
||||||
Error::Generic(format!("Failed to read Payload directory: {}", e))
|
|
||||||
})?
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.filter(|entry| entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
|
|
||||||
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "app"))
|
|
||||||
.collect();
|
|
||||||
if app_dirs.len() == 1 {
|
|
||||||
bundle_path = app_dirs[0].path();
|
|
||||||
} else if app_dirs.is_empty() {
|
|
||||||
return Err(Error::InvalidBundle(
|
|
||||||
"No .app directory found in Payload".to_string(),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
return Err(Error::InvalidBundle(
|
|
||||||
"Multiple .app directories found in Payload".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(Error::InvalidBundle(
|
|
||||||
"No Payload directory found in the application archive".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let bundle = Bundle::new(bundle_path)?;
|
|
||||||
|
|
||||||
Ok(Application {
|
|
||||||
bundle, /*temp_path*/
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
|
|
||||||
|
|
||||||
use crate::Error;
|
|
||||||
use plist::{Dictionary, Value};
|
|
||||||
use std::{
|
|
||||||
fs,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Bundle {
|
|
||||||
pub app_info: Dictionary,
|
|
||||||
pub bundle_dir: PathBuf,
|
|
||||||
pub bundle_type: BundleType,
|
|
||||||
app_extensions: Vec<Bundle>,
|
|
||||||
frameworks: Vec<Bundle>,
|
|
||||||
_libraries: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Bundle {
|
|
||||||
pub fn new(bundle_dir: PathBuf) -> Result<Self, Error> {
|
|
||||||
let mut bundle_path = bundle_dir;
|
|
||||||
// Remove trailing slash/backslash
|
|
||||||
if let Some(path_str) = bundle_path.to_str()
|
|
||||||
&& (path_str.ends_with('/') || path_str.ends_with('\\'))
|
|
||||||
{
|
|
||||||
bundle_path = PathBuf::from(&path_str[..path_str.len() - 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let info_plist_path = bundle_path.join("Info.plist");
|
|
||||||
assert_bundle(
|
|
||||||
info_plist_path.exists(),
|
|
||||||
&format!("No Info.plist here: {}", info_plist_path.display()),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let plist_data = fs::read(&info_plist_path)
|
|
||||||
.map_err(|e| Error::InvalidBundle(format!("Failed to read Info.plist: {}", e)))?;
|
|
||||||
|
|
||||||
let app_info = plist::from_bytes(&plist_data)
|
|
||||||
.map_err(|e| Error::InvalidBundle(format!("Failed to parse Info.plist: {}", e)))?;
|
|
||||||
|
|
||||||
// Load app extensions from PlugIns directory
|
|
||||||
let plug_ins_dir = bundle_path.join("PlugIns");
|
|
||||||
let app_extensions = if plug_ins_dir.exists() {
|
|
||||||
fs::read_dir(&plug_ins_dir)
|
|
||||||
.map_err(|e| {
|
|
||||||
Error::InvalidBundle(format!("Failed to read PlugIns directory: {}", e))
|
|
||||||
})?
|
|
||||||
.filter_map(|entry| entry.ok())
|
|
||||||
.filter(|entry| {
|
|
||||||
entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
|
|
||||||
&& entry.path().join("Info.plist").exists()
|
|
||||||
})
|
|
||||||
.filter_map(|entry| Bundle::new(entry.path()).ok())
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load frameworks from Frameworks directory
|
|
||||||
let frameworks_dir = bundle_path.join("Frameworks");
|
|
||||||
let frameworks = if frameworks_dir.exists() {
|
|
||||||
fs::read_dir(&frameworks_dir)
|
|
||||||
.map_err(|e| {
|
|
||||||
Error::InvalidBundle(format!("Failed to read Frameworks directory: {}", e))
|
|
||||||
})?
|
|
||||||
.filter_map(|entry| entry.ok())
|
|
||||||
.filter(|entry| {
|
|
||||||
entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
|
|
||||||
&& entry.path().join("Info.plist").exists()
|
|
||||||
})
|
|
||||||
.filter_map(|entry| Bundle::new(entry.path()).ok())
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find all .dylib files in the bundle directory (recursive)
|
|
||||||
let libraries = find_dylibs(&bundle_path, &bundle_path)?;
|
|
||||||
|
|
||||||
Ok(Bundle {
|
|
||||||
app_info,
|
|
||||||
bundle_type: BundleType::from_extension(
|
|
||||||
bundle_path
|
|
||||||
.extension()
|
|
||||||
.and_then(|ext| ext.to_str())
|
|
||||||
.unwrap_or(""),
|
|
||||||
),
|
|
||||||
bundle_dir: bundle_path,
|
|
||||||
app_extensions,
|
|
||||||
frameworks,
|
|
||||||
_libraries: libraries,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_bundle_identifier(&mut self, id: &str) {
|
|
||||||
self.app_info.insert(
|
|
||||||
"CFBundleIdentifier".to_string(),
|
|
||||||
Value::String(id.to_string()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bundle_identifier(&self) -> Option<&str> {
|
|
||||||
self.app_info
|
|
||||||
.get("CFBundleIdentifier")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bundle_name(&self) -> Option<&str> {
|
|
||||||
self.app_info
|
|
||||||
.get("CFBundleName")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn app_extensions(&self) -> &[Bundle] {
|
|
||||||
&self.app_extensions
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn app_extensions_mut(&mut self) -> &mut [Bundle] {
|
|
||||||
&mut self.app_extensions
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_info(&self) -> Result<(), Error> {
|
|
||||||
let info_plist_path = self.bundle_dir.join("Info.plist");
|
|
||||||
let result = plist::to_file_binary(&info_plist_path, &self.app_info);
|
|
||||||
|
|
||||||
if result.is_err() {
|
|
||||||
return Err(Error::InvalidBundle(format!(
|
|
||||||
"Failed to write Info.plist: {}",
|
|
||||||
result.unwrap_err()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn embedded_bundles(&self) -> Vec<&Bundle> {
|
|
||||||
let mut bundles = Vec::new();
|
|
||||||
bundles.extend(self.app_extensions.iter());
|
|
||||||
bundles.extend(self.frameworks.iter());
|
|
||||||
bundles.push(self);
|
|
||||||
bundles.sort_by_key(|b| b.bundle_dir.components().count());
|
|
||||||
bundles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_bundle(condition: bool, msg: &str) -> Result<(), Error> {
|
|
||||||
if !condition {
|
|
||||||
Err(Error::InvalidBundle(msg.to_string()))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_dylibs(dir: &Path, bundle_root: &Path) -> Result<Vec<String>, Error> {
|
|
||||||
let mut libraries = Vec::new();
|
|
||||||
|
|
||||||
fn collect_dylibs(
|
|
||||||
dir: &Path,
|
|
||||||
bundle_root: &Path,
|
|
||||||
libraries: &mut Vec<String>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let entries = fs::read_dir(dir).map_err(|e| {
|
|
||||||
Error::InvalidBundle(format!("Failed to read directory {}: {}", dir.display(), e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
for entry in entries {
|
|
||||||
let entry = entry.map_err(|e| {
|
|
||||||
Error::InvalidBundle(format!("Failed to read directory entry: {}", e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let path = entry.path();
|
|
||||||
let file_type = entry
|
|
||||||
.file_type()
|
|
||||||
.map_err(|e| Error::InvalidBundle(format!("Failed to get file type: {}", e)))?;
|
|
||||||
|
|
||||||
if file_type.is_file() {
|
|
||||||
if let Some(name) = path.file_name().and_then(|n| n.to_str())
|
|
||||||
&& name.ends_with(".dylib")
|
|
||||||
{
|
|
||||||
// Get relative path from bundle root
|
|
||||||
if let Ok(relative_path) = path.strip_prefix(bundle_root)
|
|
||||||
&& let Some(relative_str) = relative_path.to_str()
|
|
||||||
{
|
|
||||||
libraries.push(relative_str.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if file_type.is_dir() {
|
|
||||||
collect_dylibs(&path, bundle_root, libraries)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
collect_dylibs(dir, bundle_root, &mut libraries)?;
|
|
||||||
Ok(libraries)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Borrowed from https://github.com/khcrysalis/PlumeImpactor/blob/main/crates/utils/src/bundle.rs
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum BundleType {
|
|
||||||
App,
|
|
||||||
AppExtension,
|
|
||||||
Framework,
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BundleType {
|
|
||||||
pub fn from_extension(ext: &str) -> Self {
|
|
||||||
match ext {
|
|
||||||
"app" => BundleType::App,
|
|
||||||
"appex" => BundleType::AppExtension,
|
|
||||||
"framework" => BundleType::Framework,
|
|
||||||
_ => BundleType::Unknown,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
|
|
||||||
|
|
||||||
use apple_codesign::SigningSettings;
|
|
||||||
use hex;
|
|
||||||
use rcgen::{CertificateParams, DnType, KeyPair};
|
|
||||||
use rsa::{
|
|
||||||
RsaPrivateKey,
|
|
||||||
pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding},
|
|
||||||
};
|
|
||||||
use sha1::{Digest, Sha1};
|
|
||||||
use std::{
|
|
||||||
fs,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
use x509_certificate::{CapturedX509Certificate, InMemorySigningKeyPair, Sign, X509Certificate};
|
|
||||||
|
|
||||||
use crate::Error;
|
|
||||||
use crate::developer_session::{DeveloperDeviceType, DeveloperSession, DeveloperTeam};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CertificateIdentity {
|
|
||||||
pub certificate: Option<X509Certificate>,
|
|
||||||
pub key_pair: InMemorySigningKeyPair,
|
|
||||||
pub private_key: RsaPrivateKey,
|
|
||||||
pub key_file: PathBuf,
|
|
||||||
pub cert_file: PathBuf,
|
|
||||||
pub machine_name: String,
|
|
||||||
pub machine_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CertificateIdentity {
|
|
||||||
pub async fn new(
|
|
||||||
configuration_path: &Path,
|
|
||||||
dev_session: &DeveloperSession,
|
|
||||||
apple_id: String,
|
|
||||||
machine_name: String,
|
|
||||||
) -> Result<Self, Error> {
|
|
||||||
let mut hasher = Sha1::new();
|
|
||||||
hasher.update(apple_id.as_bytes());
|
|
||||||
let hash_string = hex::encode(hasher.finalize()).to_lowercase();
|
|
||||||
let key_path = configuration_path.join("keys").join(hash_string);
|
|
||||||
fs::create_dir_all(&key_path).map_err(Error::Filesystem)?;
|
|
||||||
|
|
||||||
let key_file = key_path.join("key.pem");
|
|
||||||
let cert_file = key_path.join("cert.pem");
|
|
||||||
let teams = dev_session.list_teams().await?;
|
|
||||||
let team = teams
|
|
||||||
.first()
|
|
||||||
.ok_or(Error::Certificate("No teams found".to_string()))?;
|
|
||||||
|
|
||||||
let private_key = if key_file.exists() {
|
|
||||||
let key_data = fs::read_to_string(&key_file)
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to read key file: {}", e)))?;
|
|
||||||
RsaPrivateKey::from_pkcs8_pem(&key_data)
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to load private key: {}", e)))?
|
|
||||||
} else {
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
let private_key = RsaPrivateKey::new(&mut rng, 2048)
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to generate RSA key: {}", e)))?;
|
|
||||||
|
|
||||||
let pem_data = private_key
|
|
||||||
.to_pkcs8_pem(LineEnding::LF)
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to encode private key: {}", e)))?;
|
|
||||||
fs::write(&key_file, pem_data.as_bytes()).map_err(Error::Filesystem)?;
|
|
||||||
private_key
|
|
||||||
};
|
|
||||||
|
|
||||||
let key_pair = InMemorySigningKeyPair::from_pkcs8_der(
|
|
||||||
private_key
|
|
||||||
.to_pkcs8_der()
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to encode private key: {}", e)))?
|
|
||||||
.as_bytes(),
|
|
||||||
)
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to decode private key: {}", e)))?;
|
|
||||||
|
|
||||||
let mut cert_identity = CertificateIdentity {
|
|
||||||
certificate: None,
|
|
||||||
key_pair,
|
|
||||||
private_key,
|
|
||||||
key_file,
|
|
||||||
cert_file,
|
|
||||||
machine_name,
|
|
||||||
machine_id: "".to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok((cert, machine_id)) = cert_identity
|
|
||||||
.find_matching_certificate(dev_session, team)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
cert_identity.certificate = Some(cert.clone());
|
|
||||||
cert_identity.machine_id = machine_id;
|
|
||||||
|
|
||||||
let cert_pem = cert
|
|
||||||
.encode_pem()
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to encode cert: {}", e)))?;
|
|
||||||
fs::write(&cert_identity.cert_file, cert_pem).map_err(Error::Filesystem)?;
|
|
||||||
|
|
||||||
return Ok(cert_identity);
|
|
||||||
}
|
|
||||||
|
|
||||||
cert_identity
|
|
||||||
.request_new_certificate(dev_session, team)
|
|
||||||
.await?;
|
|
||||||
Ok(cert_identity)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_matching_certificate(
|
|
||||||
&self,
|
|
||||||
dev_session: &DeveloperSession,
|
|
||||||
team: &DeveloperTeam,
|
|
||||||
) -> Result<(X509Certificate, String), Error> {
|
|
||||||
let certificates = dev_session
|
|
||||||
.list_all_development_certs(DeveloperDeviceType::Ios, team)
|
|
||||||
.await
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to list certificates: {:?}", e)))?;
|
|
||||||
|
|
||||||
let our_public_key_der = self.key_pair.public_key_data().to_vec();
|
|
||||||
|
|
||||||
for cert in certificates
|
|
||||||
.iter()
|
|
||||||
.filter(|c| c.machine_name == self.machine_name)
|
|
||||||
{
|
|
||||||
if let Ok(x509_cert) = X509Certificate::from_der(&cert.cert_content) {
|
|
||||||
let cert_public_key_der: Vec<u8> = x509_cert
|
|
||||||
.tbs_certificate()
|
|
||||||
.subject_public_key_info
|
|
||||||
.subject_public_key
|
|
||||||
.octets()
|
|
||||||
.collect();
|
|
||||||
if cert_public_key_der == our_public_key_der {
|
|
||||||
return Ok((x509_cert, cert.machine_id.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(Error::Certificate(
|
|
||||||
"No matching certificate found".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn request_new_certificate(
|
|
||||||
&mut self,
|
|
||||||
dev_session: &DeveloperSession,
|
|
||||||
team: &DeveloperTeam,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let mut params = CertificateParams::new(vec!["CN".to_string()])
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to create params: {}", e)))?;
|
|
||||||
params.distinguished_name.push(DnType::CountryName, "US");
|
|
||||||
params
|
|
||||||
.distinguished_name
|
|
||||||
.push(DnType::StateOrProvinceName, "STATE");
|
|
||||||
params
|
|
||||||
.distinguished_name
|
|
||||||
.push(DnType::LocalityName, "LOCAL");
|
|
||||||
params
|
|
||||||
.distinguished_name
|
|
||||||
.push(DnType::OrganizationName, "ORGNIZATION");
|
|
||||||
params.distinguished_name.push(DnType::CommonName, "CN");
|
|
||||||
|
|
||||||
let key_pem = self
|
|
||||||
.private_key
|
|
||||||
.to_pkcs8_pem(LineEnding::LF)
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to encode private key: {}", e)))?;
|
|
||||||
let key_pair = KeyPair::from_pem(&key_pem)
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to load key pair for CSR: {}", e)))?;
|
|
||||||
|
|
||||||
let csr = params
|
|
||||||
.serialize_request(&key_pair)
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to generate CSR: {}", e)))?;
|
|
||||||
let csr_pem = csr
|
|
||||||
.pem()
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to encode CSR to PEM: {}", e)))?;
|
|
||||||
|
|
||||||
let certificate_id = dev_session
|
|
||||||
.submit_development_csr(
|
|
||||||
DeveloperDeviceType::Ios,
|
|
||||||
team,
|
|
||||||
csr_pem,
|
|
||||||
self.machine_name.clone(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
let is_7460 = match &e {
|
|
||||||
Error::DeveloperSession(code, _) => *code == 7460,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
if is_7460 {
|
|
||||||
Error::Certificate("You have too many certificates!".to_string())
|
|
||||||
} else {
|
|
||||||
Error::Certificate(format!("Failed to submit CSR: {:?}", e))
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let certificates = dev_session
|
|
||||||
.list_all_development_certs(DeveloperDeviceType::Ios, team)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let apple_cert = certificates
|
|
||||||
.iter()
|
|
||||||
.find(|cert| cert.certificate_id == certificate_id)
|
|
||||||
.ok_or(Error::Certificate(
|
|
||||||
"Certificate not found after submission".to_string(),
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let certificate = X509Certificate::from_der(&apple_cert.cert_content)
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to parse certificate: {}", e)))?;
|
|
||||||
|
|
||||||
// Write certificate to disk
|
|
||||||
let cert_pem = certificate
|
|
||||||
.encode_pem()
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to encode cert: {}", e)))?;
|
|
||||||
fs::write(&self.cert_file, cert_pem).map_err(Error::Filesystem)?;
|
|
||||||
|
|
||||||
self.certificate = Some(certificate);
|
|
||||||
self.machine_id = apple_cert.machine_id.clone();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_certificate_file_path(&self) -> &Path {
|
|
||||||
&self.cert_file
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_private_key_file_path(&self) -> &Path {
|
|
||||||
&self.key_file
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_serial_number(&self) -> Result<String, Error> {
|
|
||||||
let cert = match &self.certificate {
|
|
||||||
Some(c) => c,
|
|
||||||
None => {
|
|
||||||
return Err(Error::Certificate(
|
|
||||||
"No certificate available to get serial number".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let serial = &cert.tbs_certificate().serial_number;
|
|
||||||
let hex_str = hex::encode(serial.as_slice());
|
|
||||||
|
|
||||||
Ok(hex_str.trim_start_matches("0").to_string().to_uppercase())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_pkcs12(&self, password: &str) -> Result<Vec<u8>, Error> {
|
|
||||||
let cert = self
|
|
||||||
.certificate
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(Error::Certificate("Certificate not found".to_string()))?;
|
|
||||||
|
|
||||||
let cert_der = cert
|
|
||||||
.encode_der()
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to encode certificate: {}", e)))?;
|
|
||||||
|
|
||||||
let key_der = self
|
|
||||||
.private_key
|
|
||||||
.to_pkcs8_der()
|
|
||||||
.map_err(|e| Error::Certificate(format!("Failed to encode private key: {}", e)))?;
|
|
||||||
|
|
||||||
let pfx = p12::PFX::new(
|
|
||||||
&cert_der,
|
|
||||||
key_der.as_bytes(),
|
|
||||||
None,
|
|
||||||
password,
|
|
||||||
&self.machine_name,
|
|
||||||
)
|
|
||||||
.ok_or(Error::Certificate("Failed to create PKCS#12".to_string()))?;
|
|
||||||
|
|
||||||
Ok(pfx.to_der())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_signing_settings(&self) -> Result<SigningSettings<'_>, Error> {
|
|
||||||
let mut settings = SigningSettings::default();
|
|
||||||
|
|
||||||
let certificate = self
|
|
||||||
.certificate
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(Error::Certificate("Certificate not found".to_string()))?;
|
|
||||||
|
|
||||||
settings.set_signing_key(
|
|
||||||
&self.key_pair,
|
|
||||||
CapturedX509Certificate::from_der(
|
|
||||||
certificate.encode_der().map_err(|e| {
|
|
||||||
Error::Certificate(format!("Failed to encode certificate: {}", e))
|
|
||||||
})?,
|
|
||||||
)
|
|
||||||
.map_err(|e| {
|
|
||||||
Error::Certificate(format!("Failed to create captured certificate: {}", e))
|
|
||||||
})?,
|
|
||||||
);
|
|
||||||
settings.chain_apple_certificates();
|
|
||||||
settings.set_for_notarization(false);
|
|
||||||
settings.set_shallow(true);
|
|
||||||
settings.set_team_id_from_signing_certificate().ok_or({
|
|
||||||
Error::Certificate("Failed to set team ID from signing certificate".to_string())
|
|
||||||
})?;
|
|
||||||
settings
|
|
||||||
.set_time_stamp_url("http://timestamp.apple.com/ts01")
|
|
||||||
.map_err(|e| Error::AppleCodesignError(Box::new(e)))?;
|
|
||||||
|
|
||||||
Ok(settings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,779 +0,0 @@
|
|||||||
// This file was made using https://github.com/Dadoum/Sideloader as a reference for the apple private endpoints
|
|
||||||
|
|
||||||
use crate::Error;
|
|
||||||
use icloud_auth::{AppleAccount, Error as ICloudError};
|
|
||||||
use plist::{Date, Dictionary, Value};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
pub struct DeveloperSession {
|
|
||||||
pub account: Arc<AppleAccount>,
|
|
||||||
team: Option<DeveloperTeam>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DeveloperSession {
|
|
||||||
pub fn new(account: Arc<AppleAccount>) -> Self {
|
|
||||||
DeveloperSession {
|
|
||||||
account,
|
|
||||||
team: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_developer_request(
|
|
||||||
&self,
|
|
||||||
url: &str,
|
|
||||||
body: Option<Dictionary>,
|
|
||||||
) -> Result<Dictionary, Error> {
|
|
||||||
let mut request = Dictionary::new();
|
|
||||||
request.insert(
|
|
||||||
"clientId".to_string(),
|
|
||||||
Value::String("XABBG36SBA".to_string()),
|
|
||||||
);
|
|
||||||
request.insert(
|
|
||||||
"protocolVersion".to_string(),
|
|
||||||
Value::String("QH65B2".to_string()),
|
|
||||||
);
|
|
||||||
request.insert(
|
|
||||||
"requestId".to_string(),
|
|
||||||
Value::String(Uuid::new_v4().to_string().to_uppercase()),
|
|
||||||
);
|
|
||||||
request.insert(
|
|
||||||
"userLocale".to_string(),
|
|
||||||
Value::Array(vec![Value::String("en_US".to_string())]),
|
|
||||||
);
|
|
||||||
if let Some(body) = body {
|
|
||||||
for (key, value) in body {
|
|
||||||
request.insert(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.account
|
|
||||||
.send_request(url, Some(request))
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
if let ICloudError::AuthSrpWithMessage(code, message) = e {
|
|
||||||
Error::DeveloperSession(code, format!("Developer request failed: {}", message))
|
|
||||||
} else {
|
|
||||||
Error::Generic("Failed to send developer request".to_string())
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let status_code = response
|
|
||||||
.get("resultCode")
|
|
||||||
.and_then(|v| v.as_unsigned_integer())
|
|
||||||
.unwrap_or(0);
|
|
||||||
if status_code != 0 {
|
|
||||||
let description = response
|
|
||||||
.get("userString")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.or_else(|| response.get("resultString").and_then(|v| v.as_string()))
|
|
||||||
.unwrap_or("(null)");
|
|
||||||
return Err(Error::DeveloperSession(
|
|
||||||
status_code as i64,
|
|
||||||
description.to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list_teams(&self) -> Result<Vec<DeveloperTeam>, Error> {
|
|
||||||
let url = "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA";
|
|
||||||
let response = self.send_developer_request(url, None).await?;
|
|
||||||
|
|
||||||
let teams = response
|
|
||||||
.get("teams")
|
|
||||||
.and_then(|v| v.as_array())
|
|
||||||
.ok_or(Error::Parse("teams".to_string()))?;
|
|
||||||
|
|
||||||
let mut result = Vec::new();
|
|
||||||
for team in teams {
|
|
||||||
let dict = team
|
|
||||||
.as_dictionary()
|
|
||||||
.ok_or(Error::Parse("team".to_string()))?;
|
|
||||||
let name = dict
|
|
||||||
.get("name")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("name".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let team_id = dict
|
|
||||||
.get("teamId")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("teamId".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
result.push(DeveloperTeam {
|
|
||||||
_name: name,
|
|
||||||
team_id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_team(&self) -> Result<DeveloperTeam, Error> {
|
|
||||||
if let Some(team) = &self.team {
|
|
||||||
return Ok(team.clone());
|
|
||||||
}
|
|
||||||
let teams = self.list_teams().await?;
|
|
||||||
if teams.is_empty() {
|
|
||||||
return Err(Error::DeveloperSession(
|
|
||||||
-1,
|
|
||||||
"No developer teams found".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
// TODO: Handle multiple teams
|
|
||||||
Ok(teams[0].clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_team(&mut self, team: DeveloperTeam) {
|
|
||||||
self.team = Some(team);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list_devices(
|
|
||||||
&self,
|
|
||||||
device_type: DeveloperDeviceType,
|
|
||||||
team: &DeveloperTeam,
|
|
||||||
) -> Result<Vec<DeveloperDevice>, Error> {
|
|
||||||
let url = dev_url(device_type, "listDevices");
|
|
||||||
let mut body = Dictionary::new();
|
|
||||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
|
||||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
|
||||||
|
|
||||||
let devices = response
|
|
||||||
.get("devices")
|
|
||||||
.and_then(|v| v.as_array())
|
|
||||||
.ok_or(Error::Parse("devices".to_string()))?;
|
|
||||||
|
|
||||||
let mut result = Vec::new();
|
|
||||||
for device in devices {
|
|
||||||
let dict = device
|
|
||||||
.as_dictionary()
|
|
||||||
.ok_or(Error::Parse("device".to_string()))?;
|
|
||||||
let device_id = dict
|
|
||||||
.get("deviceId")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("deviceId".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let name = dict
|
|
||||||
.get("name")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("name".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let device_number = dict
|
|
||||||
.get("deviceNumber")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("deviceNumber".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
result.push(DeveloperDevice {
|
|
||||||
_device_id: device_id,
|
|
||||||
_name: name,
|
|
||||||
device_number,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn add_device(
|
|
||||||
&self,
|
|
||||||
device_type: DeveloperDeviceType,
|
|
||||||
team: &DeveloperTeam,
|
|
||||||
device_name: &str,
|
|
||||||
udid: &str,
|
|
||||||
) -> Result<DeveloperDevice, Error> {
|
|
||||||
let url = dev_url(device_type, "addDevice");
|
|
||||||
let mut body = Dictionary::new();
|
|
||||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
|
||||||
body.insert("name".to_string(), Value::String(device_name.to_string()));
|
|
||||||
body.insert("deviceNumber".to_string(), Value::String(udid.to_string()));
|
|
||||||
|
|
||||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
|
||||||
|
|
||||||
let device_dict = response
|
|
||||||
.get("device")
|
|
||||||
.and_then(|v| v.as_dictionary())
|
|
||||||
.ok_or(Error::Parse("device".to_string()))?;
|
|
||||||
|
|
||||||
let device_id = device_dict
|
|
||||||
.get("deviceId")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("deviceId".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let name = device_dict
|
|
||||||
.get("name")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("name".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let device_number = device_dict
|
|
||||||
.get("deviceNumber")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("deviceNumber".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
Ok(DeveloperDevice {
|
|
||||||
_device_id: device_id,
|
|
||||||
_name: name,
|
|
||||||
device_number,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list_all_development_certs(
|
|
||||||
&self,
|
|
||||||
device_type: DeveloperDeviceType,
|
|
||||||
team: &DeveloperTeam,
|
|
||||||
) -> Result<Vec<DevelopmentCertificate>, Error> {
|
|
||||||
let url = dev_url(device_type, "listAllDevelopmentCerts");
|
|
||||||
let mut body = Dictionary::new();
|
|
||||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
|
||||||
|
|
||||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
|
||||||
|
|
||||||
let certs = response
|
|
||||||
.get("certificates")
|
|
||||||
.and_then(|v| v.as_array())
|
|
||||||
.ok_or(Error::Parse("certificates".to_string()))?;
|
|
||||||
|
|
||||||
let mut result = Vec::new();
|
|
||||||
for cert in certs {
|
|
||||||
let dict = cert
|
|
||||||
.as_dictionary()
|
|
||||||
.ok_or(Error::Parse("certificate".to_string()))?;
|
|
||||||
let name = dict
|
|
||||||
.get("name")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("name".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let certificate_id = dict
|
|
||||||
.get("certificateId")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("certificateId".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let serial_number = dict
|
|
||||||
.get("serialNumber")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("serialNumber".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let machine_name = dict
|
|
||||||
.get("machineName")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string();
|
|
||||||
let machine_id = dict
|
|
||||||
.get("machineId")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("machineId".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let cert_content = dict
|
|
||||||
.get("certContent")
|
|
||||||
.and_then(|v| v.as_data())
|
|
||||||
.ok_or(Error::Parse("certContent".to_string()))?
|
|
||||||
.to_vec();
|
|
||||||
|
|
||||||
result.push(DevelopmentCertificate {
|
|
||||||
name,
|
|
||||||
certificate_id,
|
|
||||||
serial_number,
|
|
||||||
machine_name,
|
|
||||||
machine_id,
|
|
||||||
cert_content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn revoke_development_cert(
|
|
||||||
&self,
|
|
||||||
device_type: DeveloperDeviceType,
|
|
||||||
team: &DeveloperTeam,
|
|
||||||
serial_number: &str,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let url = dev_url(device_type, "revokeDevelopmentCert");
|
|
||||||
let mut body = Dictionary::new();
|
|
||||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
|
||||||
body.insert(
|
|
||||||
"serialNumber".to_string(),
|
|
||||||
Value::String(serial_number.to_string()),
|
|
||||||
);
|
|
||||||
|
|
||||||
self.send_developer_request(&url, Some(body)).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn submit_development_csr(
|
|
||||||
&self,
|
|
||||||
device_type: DeveloperDeviceType,
|
|
||||||
team: &DeveloperTeam,
|
|
||||||
csr_content: String,
|
|
||||||
machine_name: String,
|
|
||||||
) -> Result<String, Error> {
|
|
||||||
let url = dev_url(device_type, "submitDevelopmentCSR");
|
|
||||||
let mut body = Dictionary::new();
|
|
||||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
|
||||||
body.insert("csrContent".to_string(), Value::String(csr_content));
|
|
||||||
body.insert(
|
|
||||||
"machineId".to_string(),
|
|
||||||
Value::String(uuid::Uuid::new_v4().to_string().to_uppercase()),
|
|
||||||
);
|
|
||||||
body.insert("machineName".to_string(), Value::String(machine_name));
|
|
||||||
|
|
||||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
|
||||||
let cert_dict = response
|
|
||||||
.get("certRequest")
|
|
||||||
.and_then(|v| v.as_dictionary())
|
|
||||||
.ok_or(Error::Parse("certRequest".to_string()))?;
|
|
||||||
let id = cert_dict
|
|
||||||
.get("certRequestId")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("certRequestId".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list_app_ids(
|
|
||||||
&self,
|
|
||||||
device_type: DeveloperDeviceType,
|
|
||||||
team: &DeveloperTeam,
|
|
||||||
) -> Result<ListAppIdsResponse, Error> {
|
|
||||||
let url = dev_url(device_type, "listAppIds");
|
|
||||||
let mut body = Dictionary::new();
|
|
||||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
|
||||||
|
|
||||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
|
||||||
|
|
||||||
let app_ids = response
|
|
||||||
.get("appIds")
|
|
||||||
.and_then(|v| v.as_array())
|
|
||||||
.ok_or(Error::Parse("appIds".to_string()))?;
|
|
||||||
|
|
||||||
let mut result = Vec::new();
|
|
||||||
for app_id in app_ids {
|
|
||||||
let dict = app_id
|
|
||||||
.as_dictionary()
|
|
||||||
.ok_or(Error::Parse("appId".to_string()))?;
|
|
||||||
let name = dict
|
|
||||||
.get("name")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("name".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let app_id_id = dict
|
|
||||||
.get("appIdId")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("appIdId".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let identifier = dict
|
|
||||||
.get("identifier")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("identifier".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let features = dict
|
|
||||||
.get("features")
|
|
||||||
.and_then(|v| v.as_dictionary())
|
|
||||||
.ok_or(Error::Parse("features".to_string()))?;
|
|
||||||
let expiration_date = if dict.contains_key("expirationDate") {
|
|
||||||
Some(
|
|
||||||
dict.get("expirationDate")
|
|
||||||
.and_then(|v| v.as_date())
|
|
||||||
.ok_or(Error::Parse("expirationDate".to_string()))?,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
result.push(AppId {
|
|
||||||
name,
|
|
||||||
app_id_id,
|
|
||||||
identifier,
|
|
||||||
features: features.clone(),
|
|
||||||
expiration_date,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let max_quantity = if response.contains_key("maxQuantity") {
|
|
||||||
Some(
|
|
||||||
response
|
|
||||||
.get("maxQuantity")
|
|
||||||
.and_then(|v| v.as_unsigned_integer())
|
|
||||||
.ok_or(Error::Parse("maxQuantity".to_string()))?,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let available_quantity = if response.contains_key("availableQuantity") {
|
|
||||||
Some(
|
|
||||||
response
|
|
||||||
.get("availableQuantity")
|
|
||||||
.and_then(|v| v.as_unsigned_integer())
|
|
||||||
.ok_or(Error::Parse("availableQuantity".to_string()))?,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ListAppIdsResponse {
|
|
||||||
app_ids: result,
|
|
||||||
max_quantity,
|
|
||||||
available_quantity,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn add_app_id(
|
|
||||||
&self,
|
|
||||||
device_type: DeveloperDeviceType,
|
|
||||||
team: &DeveloperTeam,
|
|
||||||
name: &str,
|
|
||||||
identifier: &str,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let url = dev_url(device_type, "addAppId");
|
|
||||||
let mut body = Dictionary::new();
|
|
||||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
|
||||||
body.insert("name".to_string(), Value::String(name.to_string()));
|
|
||||||
body.insert(
|
|
||||||
"identifier".to_string(),
|
|
||||||
Value::String(identifier.to_string()),
|
|
||||||
);
|
|
||||||
|
|
||||||
self.send_developer_request(&url, Some(body)).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_app_id(
|
|
||||||
&self,
|
|
||||||
device_type: DeveloperDeviceType,
|
|
||||||
team: &DeveloperTeam,
|
|
||||||
app_id: &AppId,
|
|
||||||
features: &Dictionary,
|
|
||||||
) -> Result<Dictionary, Error> {
|
|
||||||
let url = dev_url(device_type, "updateAppId");
|
|
||||||
let mut body = Dictionary::new();
|
|
||||||
body.insert(
|
|
||||||
"appIdId".to_string(),
|
|
||||||
Value::String(app_id.app_id_id.clone()),
|
|
||||||
);
|
|
||||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
|
||||||
|
|
||||||
for (key, value) in features {
|
|
||||||
body.insert(key.clone(), value.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
|
||||||
let cert_dict = response
|
|
||||||
.get("appId")
|
|
||||||
.and_then(|v| v.as_dictionary())
|
|
||||||
.ok_or(Error::Parse("appId".to_string()))?;
|
|
||||||
let feats = cert_dict
|
|
||||||
.get("features")
|
|
||||||
.and_then(|v| v.as_dictionary())
|
|
||||||
.ok_or(Error::Parse("features".to_string()))?;
|
|
||||||
|
|
||||||
Ok(feats.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_app_id(
|
|
||||||
&self,
|
|
||||||
device_type: DeveloperDeviceType,
|
|
||||||
team: &DeveloperTeam,
|
|
||||||
app_id_id: String,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let url = dev_url(device_type, "deleteAppId");
|
|
||||||
let mut body = Dictionary::new();
|
|
||||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
|
||||||
body.insert("appIdId".to_string(), Value::String(app_id_id.clone()));
|
|
||||||
|
|
||||||
self.send_developer_request(&url, Some(body)).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list_application_groups(
|
|
||||||
&self,
|
|
||||||
device_type: DeveloperDeviceType,
|
|
||||||
team: &DeveloperTeam,
|
|
||||||
) -> Result<Vec<ApplicationGroup>, Error> {
|
|
||||||
let url = dev_url(device_type, "listApplicationGroups");
|
|
||||||
let mut body = Dictionary::new();
|
|
||||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
|
||||||
|
|
||||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
|
||||||
|
|
||||||
let app_groups = response
|
|
||||||
.get("applicationGroupList")
|
|
||||||
.and_then(|v| v.as_array())
|
|
||||||
.ok_or(Error::Parse("applicationGroupList".to_string()))?;
|
|
||||||
|
|
||||||
let mut result = Vec::new();
|
|
||||||
for app_group in app_groups {
|
|
||||||
let dict = app_group
|
|
||||||
.as_dictionary()
|
|
||||||
.ok_or(Error::Parse("applicationGroup".to_string()))?;
|
|
||||||
let application_group = dict
|
|
||||||
.get("applicationGroup")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("applicationGroup".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let name = dict
|
|
||||||
.get("name")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("name".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let identifier = dict
|
|
||||||
.get("identifier")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("identifier".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
result.push(ApplicationGroup {
|
|
||||||
application_group,
|
|
||||||
_name: name,
|
|
||||||
identifier,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn add_application_group(
|
|
||||||
&self,
|
|
||||||
device_type: DeveloperDeviceType,
|
|
||||||
team: &DeveloperTeam,
|
|
||||||
group_identifier: &str,
|
|
||||||
name: &str,
|
|
||||||
) -> Result<ApplicationGroup, Error> {
|
|
||||||
let url = dev_url(device_type, "addApplicationGroup");
|
|
||||||
let mut body = Dictionary::new();
|
|
||||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
|
||||||
body.insert("name".to_string(), Value::String(name.to_string()));
|
|
||||||
body.insert(
|
|
||||||
"identifier".to_string(),
|
|
||||||
Value::String(group_identifier.to_string()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
|
||||||
let app_group_dict = response
|
|
||||||
.get("applicationGroup")
|
|
||||||
.and_then(|v| v.as_dictionary())
|
|
||||||
.ok_or(Error::Parse("applicationGroup".to_string()))?;
|
|
||||||
let application_group = app_group_dict
|
|
||||||
.get("applicationGroup")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("applicationGroup".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let name = app_group_dict
|
|
||||||
.get("name")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("name".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let identifier = app_group_dict
|
|
||||||
.get("identifier")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("identifier".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
Ok(ApplicationGroup {
|
|
||||||
application_group,
|
|
||||||
_name: name,
|
|
||||||
identifier,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn assign_application_group_to_app_id(
|
|
||||||
&self,
|
|
||||||
device_type: DeveloperDeviceType,
|
|
||||||
team: &DeveloperTeam,
|
|
||||||
app_id: &AppId,
|
|
||||||
app_group: &ApplicationGroup,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let url = dev_url(device_type, "assignApplicationGroupToAppId");
|
|
||||||
let mut body = Dictionary::new();
|
|
||||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
|
||||||
body.insert(
|
|
||||||
"appIdId".to_string(),
|
|
||||||
Value::String(app_id.app_id_id.clone()),
|
|
||||||
);
|
|
||||||
body.insert(
|
|
||||||
"applicationGroups".to_string(),
|
|
||||||
Value::String(app_group.application_group.clone()),
|
|
||||||
);
|
|
||||||
|
|
||||||
self.send_developer_request(&url, Some(body)).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn download_team_provisioning_profile(
|
|
||||||
&self,
|
|
||||||
device_type: DeveloperDeviceType,
|
|
||||||
team: &DeveloperTeam,
|
|
||||||
app_id: &AppId,
|
|
||||||
) -> Result<ProvisioningProfile, Error> {
|
|
||||||
let url = dev_url(device_type, "downloadTeamProvisioningProfile");
|
|
||||||
let mut body = Dictionary::new();
|
|
||||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
|
||||||
body.insert(
|
|
||||||
"appIdId".to_string(),
|
|
||||||
Value::String(app_id.app_id_id.clone()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
|
||||||
|
|
||||||
let profile = response
|
|
||||||
.get("provisioningProfile")
|
|
||||||
.and_then(|v| v.as_dictionary())
|
|
||||||
.ok_or(Error::Parse("provisioningProfile".to_string()))?;
|
|
||||||
let name = profile
|
|
||||||
.get("name")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("name".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let provisioning_profile_id = profile
|
|
||||||
.get("provisioningProfileId")
|
|
||||||
.and_then(|v| v.as_string())
|
|
||||||
.ok_or(Error::Parse("provisioningProfileId".to_string()))?
|
|
||||||
.to_string();
|
|
||||||
let encoded_profile = profile
|
|
||||||
.get("encodedProfile")
|
|
||||||
.and_then(|v| v.as_data())
|
|
||||||
.ok_or(Error::Parse("encodedProfile".to_string()))?
|
|
||||||
.to_vec();
|
|
||||||
|
|
||||||
Ok(ProvisioningProfile {
|
|
||||||
_name: name,
|
|
||||||
_provisioning_profile_id: provisioning_profile_id,
|
|
||||||
encoded_profile,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum DeveloperDeviceType {
|
|
||||||
Any,
|
|
||||||
Ios,
|
|
||||||
Tvos,
|
|
||||||
Watchos,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DeveloperDeviceType {
|
|
||||||
pub fn url_segment(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
DeveloperDeviceType::Any => "",
|
|
||||||
DeveloperDeviceType::Ios => "ios/",
|
|
||||||
DeveloperDeviceType::Tvos => "tvos/",
|
|
||||||
DeveloperDeviceType::Watchos => "watchos/",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dev_url(device_type: DeveloperDeviceType, endpoint: &str) -> String {
|
|
||||||
format!(
|
|
||||||
"https://developerservices2.apple.com/services/QH65B2/{}{}.action?clientId=XABBG36SBA",
|
|
||||||
device_type.url_segment(),
|
|
||||||
endpoint
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DeveloperDevice {
|
|
||||||
pub _device_id: String,
|
|
||||||
pub _name: String,
|
|
||||||
pub device_number: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DeveloperTeam {
|
|
||||||
pub _name: String,
|
|
||||||
pub team_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DevelopmentCertificate {
|
|
||||||
pub name: String,
|
|
||||||
pub certificate_id: String,
|
|
||||||
pub serial_number: String,
|
|
||||||
pub machine_name: String,
|
|
||||||
pub machine_id: String,
|
|
||||||
pub cert_content: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct AppId {
|
|
||||||
pub app_id_id: String,
|
|
||||||
pub identifier: String,
|
|
||||||
pub name: String,
|
|
||||||
pub features: Dictionary,
|
|
||||||
pub expiration_date: Option<Date>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ListAppIdsResponse {
|
|
||||||
pub app_ids: Vec<AppId>,
|
|
||||||
pub max_quantity: Option<u64>,
|
|
||||||
pub available_quantity: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ApplicationGroup {
|
|
||||||
pub application_group: String,
|
|
||||||
pub _name: String,
|
|
||||||
pub identifier: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ProvisioningProfile {
|
|
||||||
pub _provisioning_profile_id: String,
|
|
||||||
pub _name: String,
|
|
||||||
pub encoded_profile: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProvisioningProfile {
|
|
||||||
// TODO: I'm not sure if this is the proper way to parse this but it works so...
|
|
||||||
pub fn profile_plist(&self) -> Result<plist::Dictionary, Error> {
|
|
||||||
let start_marker = b"<?xml";
|
|
||||||
let end_marker = b"</plist>";
|
|
||||||
|
|
||||||
let start = self
|
|
||||||
.encoded_profile
|
|
||||||
.windows(start_marker.len())
|
|
||||||
.position(|w| w == start_marker)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
Error::Generic("Failed to find start of plist in provisioning profile".to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let end = self
|
|
||||||
.encoded_profile
|
|
||||||
.windows(end_marker.len())
|
|
||||||
.position(|w| w == end_marker)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
Error::Generic("Failed to find end of plist in provisioning profile".to_string())
|
|
||||||
})?
|
|
||||||
+ end_marker.len();
|
|
||||||
|
|
||||||
plist::from_bytes::<plist::Dictionary>(&self.encoded_profile[start..end]).map_err(|e| {
|
|
||||||
Error::Generic(format!("Failed to parse provisioning profile plist: {}", e))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn entitlements_xml(&self) -> Result<String, Error> {
|
|
||||||
let profile_plist = self.profile_plist()?;
|
|
||||||
let entitlements = profile_plist.get("Entitlements").ok_or_else(|| {
|
|
||||||
Error::Generic("No Entitlements found in provisioning profile".to_string())
|
|
||||||
})?;
|
|
||||||
let mut buf = vec![];
|
|
||||||
entitlements.to_writer_xml(&mut buf).map_err(|e| {
|
|
||||||
Error::Generic(format!(
|
|
||||||
"Failed to convert entitlements to XML for codesigning: {}",
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
let entitlements = std::str::from_utf8(&buf)
|
|
||||||
.map_err(|e| {
|
|
||||||
Error::Generic(format!(
|
|
||||||
"Failed to convert entitlements to UTF-8 for codesigning: {}",
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
Ok(entitlements)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
use idevice::{
|
|
||||||
IdeviceService, afc::AfcClient, installation_proxy::InstallationProxyClient,
|
|
||||||
provider::IdeviceProvider,
|
|
||||||
};
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::{future::Future, path::Path};
|
|
||||||
|
|
||||||
use crate::Error;
|
|
||||||
|
|
||||||
/// Installs an ***already signed*** app onto your device.
|
|
||||||
/// To sign and install an app, see [`crate::sideload::sideload_app`]
|
|
||||||
pub async fn install_app(
|
|
||||||
provider: &impl IdeviceProvider,
|
|
||||||
app_path: &Path,
|
|
||||||
progress_callback: impl Fn(u64),
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let mut afc_client = AfcClient::connect(provider)
|
|
||||||
.await
|
|
||||||
.map_err(Error::IdeviceError)?;
|
|
||||||
|
|
||||||
let dir = format!(
|
|
||||||
"PublicStaging/{}",
|
|
||||||
app_path.file_name().unwrap().to_string_lossy()
|
|
||||||
);
|
|
||||||
afc_upload_dir(&mut afc_client, app_path, &dir).await?;
|
|
||||||
|
|
||||||
let mut instproxy_client = InstallationProxyClient::connect(provider)
|
|
||||||
.await
|
|
||||||
.map_err(Error::IdeviceError)?;
|
|
||||||
|
|
||||||
let mut options = plist::Dictionary::new();
|
|
||||||
options.insert("PackageType".to_string(), "Developer".into());
|
|
||||||
instproxy_client
|
|
||||||
.install_with_callback(
|
|
||||||
dir,
|
|
||||||
Some(plist::Value::Dictionary(options)),
|
|
||||||
async |(percentage, _)| {
|
|
||||||
progress_callback(percentage);
|
|
||||||
},
|
|
||||||
(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(Error::IdeviceError)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn afc_upload_dir<'a>(
|
|
||||||
afc_client: &'a mut AfcClient,
|
|
||||||
path: &'a Path,
|
|
||||||
afc_path: &'a str,
|
|
||||||
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
|
|
||||||
Box::pin(async move {
|
|
||||||
let entries = std::fs::read_dir(path).map_err(Error::Filesystem)?;
|
|
||||||
afc_client
|
|
||||||
.mk_dir(afc_path)
|
|
||||||
.await
|
|
||||||
.map_err(Error::IdeviceError)?;
|
|
||||||
for entry in entries {
|
|
||||||
let entry = entry.map_err(Error::Filesystem)?;
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
let new_afc_path = format!(
|
|
||||||
"{}/{}",
|
|
||||||
afc_path,
|
|
||||||
path.file_name().unwrap().to_string_lossy()
|
|
||||||
);
|
|
||||||
afc_upload_dir(afc_client, &path, &new_afc_path).await?;
|
|
||||||
} else {
|
|
||||||
let mut file_handle = afc_client
|
|
||||||
.open(
|
|
||||||
format!(
|
|
||||||
"{}/{}",
|
|
||||||
afc_path,
|
|
||||||
path.file_name().unwrap().to_string_lossy()
|
|
||||||
),
|
|
||||||
idevice::afc::opcode::AfcFopenMode::WrOnly,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(Error::IdeviceError)?;
|
|
||||||
let bytes = std::fs::read(&path).map_err(Error::Filesystem)?;
|
|
||||||
file_handle
|
|
||||||
.write_entire(&bytes)
|
|
||||||
.await
|
|
||||||
.map_err(Error::IdeviceError)?;
|
|
||||||
file_handle.close().await.map_err(Error::IdeviceError)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,123 +1,14 @@
|
|||||||
pub mod application;
|
pub fn add(left: u64, right: u64) -> u64 {
|
||||||
pub mod bundle;
|
left + right
|
||||||
pub mod certificate;
|
|
||||||
pub mod developer_session;
|
|
||||||
pub mod device;
|
|
||||||
pub mod sideload;
|
|
||||||
|
|
||||||
use std::io::Error as IOError;
|
|
||||||
|
|
||||||
use apple_codesign::AppleCodesignError;
|
|
||||||
pub use icloud_auth::{AnisetteConfiguration, AppleAccount};
|
|
||||||
|
|
||||||
use developer_session::DeveloperTeam;
|
|
||||||
use idevice::IdeviceError;
|
|
||||||
use thiserror::Error as ThisError;
|
|
||||||
|
|
||||||
#[derive(Debug, ThisError)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("Authentication error {0}: {1}")]
|
|
||||||
Auth(i64, String),
|
|
||||||
#[error("Developer session error {0}: {1}")]
|
|
||||||
DeveloperSession(i64, String),
|
|
||||||
#[error("Error: {0}")]
|
|
||||||
Generic(String),
|
|
||||||
#[error("Failed to parse: {0}")]
|
|
||||||
Parse(String),
|
|
||||||
#[error("Invalid bundle: {0}")]
|
|
||||||
InvalidBundle(String),
|
|
||||||
#[error("Certificate error: {0}")]
|
|
||||||
Certificate(String),
|
|
||||||
#[error(transparent)]
|
|
||||||
Filesystem(#[from] IOError),
|
|
||||||
#[error(transparent)]
|
|
||||||
IdeviceError(#[from] IdeviceError),
|
|
||||||
#[error(transparent)]
|
|
||||||
AppleCodesignError(#[from] Box<AppleCodesignError>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait SideloadLogger: Send + Sync {
|
#[cfg(test)]
|
||||||
fn log(&self, message: &str);
|
mod tests {
|
||||||
fn error(&self, error: &Error);
|
use super::*;
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DefaultLogger;
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
impl SideloadLogger for DefaultLogger {
|
let result = add(2, 2);
|
||||||
fn log(&self, message: &str) {
|
assert_eq!(result, 4);
|
||||||
println!("{message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn error(&self, error: &Error) {
|
|
||||||
eprintln!("Error: {}", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sideload configuration options.
|
|
||||||
pub struct SideloadConfiguration<'a> {
|
|
||||||
pub machine_name: String,
|
|
||||||
pub logger: &'a dyn SideloadLogger,
|
|
||||||
pub store_dir: std::path::PathBuf,
|
|
||||||
pub revoke_cert: bool,
|
|
||||||
pub force_sidestore: bool,
|
|
||||||
pub skip_register_extensions: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SideloadConfiguration<'_> {
|
|
||||||
fn default() -> Self {
|
|
||||||
SideloadConfiguration::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> SideloadConfiguration<'a> {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
SideloadConfiguration {
|
|
||||||
machine_name: "isideload".to_string(),
|
|
||||||
logger: &DefaultLogger,
|
|
||||||
store_dir: std::env::current_dir().unwrap(),
|
|
||||||
revoke_cert: false,
|
|
||||||
force_sidestore: false,
|
|
||||||
skip_register_extensions: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An arbitrary machine name to appear on the certificate (e.x. "CrossCode")
|
|
||||||
pub fn set_machine_name(mut self, machine_name: String) -> Self {
|
|
||||||
self.machine_name = machine_name;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Logger for reporting progress and errors
|
|
||||||
pub fn set_logger(mut self, logger: &'a dyn SideloadLogger) -> Self {
|
|
||||||
self.logger = logger;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Directory used to store intermediate artifacts (profiles, certs, etc.). This directory will not be cleared at the end.
|
|
||||||
pub fn set_store_dir(mut self, store_dir: std::path::PathBuf) -> Self {
|
|
||||||
self.store_dir = store_dir;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether or not to revoke the certificate immediately after installation
|
|
||||||
#[deprecated(
|
|
||||||
since = "0.1.0",
|
|
||||||
note = "Certificates will now be placed in SideStore automatically so there is no need to revoke"
|
|
||||||
)]
|
|
||||||
pub fn set_revoke_cert(mut self, revoke_cert: bool) -> Self {
|
|
||||||
self.revoke_cert = revoke_cert;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether or not to treat the app as SideStore (fixes LiveContainer+SideStore issues)
|
|
||||||
pub fn set_force_sidestore(mut self, force: bool) -> Self {
|
|
||||||
self.force_sidestore = force;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether or not to skip registering app extensions (save app IDs, default true)
|
|
||||||
pub fn set_skip_register_extensions(mut self, skip: bool) -> Self {
|
|
||||||
self.skip_register_extensions = skip;
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,463 +0,0 @@
|
|||||||
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
|
|
||||||
|
|
||||||
use apple_codesign::{SettingsScope, UnifiedSigner};
|
|
||||||
use idevice::IdeviceService;
|
|
||||||
use idevice::lockdown::LockdownClient;
|
|
||||||
use idevice::provider::IdeviceProvider;
|
|
||||||
|
|
||||||
use crate::application::Application;
|
|
||||||
use crate::developer_session::ProvisioningProfile;
|
|
||||||
use crate::device::install_app;
|
|
||||||
use crate::{DeveloperTeam, Error, SideloadConfiguration, SideloadLogger};
|
|
||||||
use crate::{
|
|
||||||
certificate::CertificateIdentity,
|
|
||||||
developer_session::{DeveloperDeviceType, DeveloperSession},
|
|
||||||
};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::{io::Write, path::PathBuf};
|
|
||||||
|
|
||||||
fn error_and_return(logger: &dyn SideloadLogger, error: Error) -> Result<(), Error> {
|
|
||||||
logger.error(&error);
|
|
||||||
Err(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Signs and installs an `.ipa` or `.app` onto a device.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// - `device_provider` - [`idevice::provider::IdeviceProvider`] for the device
|
|
||||||
/// - `dev_session` - Authenticated Apple developer session ([`crate::developer_session::DeveloperSession`]).
|
|
||||||
/// - `app_path` - Path to the `.ipa` file or `.app` bundle to sign and install
|
|
||||||
/// - `config` - Sideload configuration options ([`crate::SideloadConfiguration`])
|
|
||||||
pub async fn sideload_app(
|
|
||||||
device_provider: &impl IdeviceProvider,
|
|
||||||
dev_session: &DeveloperSession,
|
|
||||||
app_path: PathBuf,
|
|
||||||
config: SideloadConfiguration<'_>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let logger = config.logger;
|
|
||||||
let mut lockdown_client = match LockdownClient::connect(device_provider).await {
|
|
||||||
Ok(l) => l,
|
|
||||||
Err(e) => {
|
|
||||||
return error_and_return(logger, Error::IdeviceError(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(pairing_file) = device_provider.get_pairing_file().await {
|
|
||||||
lockdown_client
|
|
||||||
.start_session(&pairing_file)
|
|
||||||
.await
|
|
||||||
.map_err(Error::IdeviceError)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let device_name = lockdown_client
|
|
||||||
.get_value(Some("DeviceName"), None)
|
|
||||||
.await
|
|
||||||
.map_err(Error::IdeviceError)?
|
|
||||||
.as_string()
|
|
||||||
.ok_or(Error::Generic(
|
|
||||||
"Failed to convert DeviceName to string".to_string(),
|
|
||||||
))?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let device_uuid = lockdown_client
|
|
||||||
.get_value(Some("UniqueDeviceID"), None)
|
|
||||||
.await
|
|
||||||
.map_err(Error::IdeviceError)?
|
|
||||||
.as_string()
|
|
||||||
.ok_or(Error::Generic(
|
|
||||||
"Failed to convert UniqueDeviceID to string".to_string(),
|
|
||||||
))?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let team = match dev_session.get_team().await {
|
|
||||||
Ok(t) => t,
|
|
||||||
Err(e) => {
|
|
||||||
return error_and_return(logger, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.log("Successfully retrieved team");
|
|
||||||
|
|
||||||
ensure_device_registered(logger, dev_session, &team, &device_uuid, &device_name).await?;
|
|
||||||
|
|
||||||
let cert = match CertificateIdentity::new(
|
|
||||||
&config.store_dir,
|
|
||||||
dev_session,
|
|
||||||
dev_session.account.apple_id.clone(),
|
|
||||||
config.machine_name,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(c) => c,
|
|
||||||
Err(e) => {
|
|
||||||
return error_and_return(logger, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.log("Successfully acquired certificate");
|
|
||||||
|
|
||||||
let mut list_app_id_response = match dev_session
|
|
||||||
.list_app_ids(DeveloperDeviceType::Ios, &team)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(ids) => ids,
|
|
||||||
Err(e) => {
|
|
||||||
return error_and_return(logger, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut app = Application::new(app_path)?;
|
|
||||||
let is_sidestore = config.force_sidestore
|
|
||||||
|| app.bundle.bundle_identifier().unwrap_or("") == "com.SideStore.SideStore";
|
|
||||||
let main_app_bundle_id = match app.bundle.bundle_identifier() {
|
|
||||||
Some(id) => id.to_string(),
|
|
||||||
None => {
|
|
||||||
return error_and_return(
|
|
||||||
logger,
|
|
||||||
Error::InvalidBundle("No bundle identifier found in IPA".to_string()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let main_app_id_str = format!("{}.{}", main_app_bundle_id, team.team_id);
|
|
||||||
let main_app_name = match app.bundle.bundle_name() {
|
|
||||||
Some(name) => name.to_string(),
|
|
||||||
None => {
|
|
||||||
return error_and_return(
|
|
||||||
logger,
|
|
||||||
Error::InvalidBundle("No bundle name found in IPA".to_string()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let extensions = app.bundle.app_extensions_mut();
|
|
||||||
// for each extension, ensure it has a unique bundle identifier that starts with the main app's bundle identifier
|
|
||||||
for ext in extensions.iter_mut() {
|
|
||||||
if let Some(id) = ext.bundle_identifier() {
|
|
||||||
if !(id.starts_with(&main_app_bundle_id) && id.len() > main_app_bundle_id.len()) {
|
|
||||||
return error_and_return(
|
|
||||||
logger,
|
|
||||||
Error::InvalidBundle(format!(
|
|
||||||
"Extension {} is not part of the main app bundle identifier: {}",
|
|
||||||
ext.bundle_name().unwrap_or("Unknown"),
|
|
||||||
id
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ext.set_bundle_identifier(&format!(
|
|
||||||
"{}{}",
|
|
||||||
main_app_id_str,
|
|
||||||
&id[main_app_bundle_id.len()..]
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
app.bundle.set_bundle_identifier(&main_app_id_str);
|
|
||||||
|
|
||||||
let extension_refs: Vec<_> = app.bundle.app_extensions().iter().collect();
|
|
||||||
let mut bundles_with_app_id = vec![&app.bundle];
|
|
||||||
bundles_with_app_id.extend(extension_refs);
|
|
||||||
|
|
||||||
let app_ids_to_register = bundles_with_app_id
|
|
||||||
.iter()
|
|
||||||
.filter(|bundle| {
|
|
||||||
let bundle_id = bundle.bundle_identifier().unwrap_or("");
|
|
||||||
!list_app_id_response
|
|
||||||
.app_ids
|
|
||||||
.iter()
|
|
||||||
.any(|app_id| app_id.identifier == bundle_id)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if let Some(available) = list_app_id_response.available_quantity
|
|
||||||
&& app_ids_to_register.len() > available.try_into().unwrap()
|
|
||||||
{
|
|
||||||
return error_and_return(
|
|
||||||
logger,
|
|
||||||
Error::InvalidBundle(format!(
|
|
||||||
"This app requires {} app ids, but you only have {} available",
|
|
||||||
app_ids_to_register.len(),
|
|
||||||
available
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for bundle in app_ids_to_register {
|
|
||||||
let id = bundle.bundle_identifier().unwrap_or("");
|
|
||||||
let name = bundle.bundle_name().unwrap_or("");
|
|
||||||
if let Err(e) = dev_session
|
|
||||||
.add_app_id(DeveloperDeviceType::Ios, &team, name, id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
return error_and_return(logger, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
list_app_id_response = match dev_session
|
|
||||||
.list_app_ids(DeveloperDeviceType::Ios, &team)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(ids) => ids,
|
|
||||||
Err(e) => {
|
|
||||||
return error_and_return(logger, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut app_ids: Vec<_> = list_app_id_response
|
|
||||||
.app_ids
|
|
||||||
.into_iter()
|
|
||||||
.filter(|app_id| {
|
|
||||||
bundles_with_app_id
|
|
||||||
.iter()
|
|
||||||
.any(|bundle| app_id.identifier == bundle.bundle_identifier().unwrap_or(""))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let main_app_id = match app_ids
|
|
||||||
.iter()
|
|
||||||
.find(|app_id| app_id.identifier == main_app_id_str)
|
|
||||||
.cloned()
|
|
||||||
{
|
|
||||||
Some(id) => id,
|
|
||||||
None => {
|
|
||||||
return error_and_return(
|
|
||||||
logger,
|
|
||||||
Error::Generic(format!(
|
|
||||||
"Main app ID {} not found in registered app IDs",
|
|
||||||
main_app_id_str
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.log("Successfully registered app IDs");
|
|
||||||
|
|
||||||
for app_id in app_ids.iter_mut() {
|
|
||||||
let app_group_feature_enabled = app_id
|
|
||||||
.features
|
|
||||||
.get(
|
|
||||||
"APG3427HIY", /* Gotta love apple and their magic strings! */
|
|
||||||
)
|
|
||||||
.and_then(|v| v.as_boolean())
|
|
||||||
.ok_or(Error::Generic(
|
|
||||||
"App group feature not found in app id".to_string(),
|
|
||||||
))?;
|
|
||||||
if !app_group_feature_enabled {
|
|
||||||
let mut body = plist::Dictionary::new();
|
|
||||||
body.insert("APG3427HIY".to_string(), plist::Value::Boolean(true));
|
|
||||||
let new_features = match dev_session
|
|
||||||
.update_app_id(DeveloperDeviceType::Ios, &team, app_id, &body)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(new_feats) => new_feats,
|
|
||||||
Err(e) => {
|
|
||||||
return error_and_return(logger, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
app_id.features = new_features;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let group_identifier = format!(
|
|
||||||
"group.{}",
|
|
||||||
if config.force_sidestore {
|
|
||||||
format!("com.SideStore.SideStore.{}", team.team_id)
|
|
||||||
} else {
|
|
||||||
main_app_id_str.clone()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if is_sidestore {
|
|
||||||
app.bundle.app_info.insert(
|
|
||||||
"ALTAppGroups".to_string(),
|
|
||||||
plist::Value::Array(vec![plist::Value::String(group_identifier.clone())]),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.bundle.app_info.insert(
|
|
||||||
"ALTCertificateID".to_string(),
|
|
||||||
plist::Value::String(cert.get_serial_number().unwrap()),
|
|
||||||
);
|
|
||||||
|
|
||||||
match cert.to_pkcs12(&cert.machine_id) {
|
|
||||||
Ok(p12_bytes) => {
|
|
||||||
let alt_cert_path = app.bundle.bundle_dir.join("ALTCertificate.p12");
|
|
||||||
if alt_cert_path.exists() {
|
|
||||||
std::fs::remove_file(&alt_cert_path).map_err(Error::Filesystem)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut file = std::fs::File::create(&alt_cert_path).map_err(Error::Filesystem)?;
|
|
||||||
file.write_all(&p12_bytes).map_err(Error::Filesystem)?;
|
|
||||||
}
|
|
||||||
Err(e) => return error_and_return(logger, e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let app_groups = match dev_session
|
|
||||||
.list_application_groups(DeveloperDeviceType::Ios, &team)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(groups) => groups,
|
|
||||||
Err(e) => {
|
|
||||||
return error_and_return(logger, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let matching_app_groups = app_groups
|
|
||||||
.iter()
|
|
||||||
.filter(|group| group.identifier == group_identifier.clone())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let app_group = if matching_app_groups.is_empty() {
|
|
||||||
match dev_session
|
|
||||||
.add_application_group(
|
|
||||||
DeveloperDeviceType::Ios,
|
|
||||||
&team,
|
|
||||||
&group_identifier,
|
|
||||||
&main_app_name,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(group) => group,
|
|
||||||
Err(e) => {
|
|
||||||
return error_and_return(logger, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
matching_app_groups[0].clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut provisioning_profiles: HashMap<String, ProvisioningProfile> = HashMap::new();
|
|
||||||
for app_id in app_ids {
|
|
||||||
let assign_res = dev_session
|
|
||||||
.assign_application_group_to_app_id(
|
|
||||||
DeveloperDeviceType::Ios,
|
|
||||||
&team,
|
|
||||||
&app_id,
|
|
||||||
&app_group,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
if assign_res.is_err() {
|
|
||||||
return error_and_return(logger, assign_res.err().unwrap());
|
|
||||||
}
|
|
||||||
let provisioning_profile = match dev_session
|
|
||||||
.download_team_provisioning_profile(DeveloperDeviceType::Ios, &team, &app_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(pp /* tee hee */) => pp,
|
|
||||||
Err(e) => {
|
|
||||||
return error_and_return(logger, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
provisioning_profiles.insert(app_id.identifier.clone(), provisioning_profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.log("Successfully registered app groups");
|
|
||||||
|
|
||||||
let profile_path = app.bundle.bundle_dir.join("embedded.mobileprovision");
|
|
||||||
|
|
||||||
if profile_path.exists() {
|
|
||||||
std::fs::remove_file(&profile_path).map_err(Error::Filesystem)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut file = std::fs::File::create(&profile_path).map_err(Error::Filesystem)?;
|
|
||||||
file.write_all(&provisioning_profile.encoded_profile)
|
|
||||||
.map_err(Error::Filesystem)?;
|
|
||||||
|
|
||||||
// Without this, zsign complains it can't find the provision file
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
file.sync_all().map_err(|e| Error::Filesystem(e))?;
|
|
||||||
drop(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.bundle.write_info()?;
|
|
||||||
for ext in app.bundle.app_extensions_mut() {
|
|
||||||
ext.write_info()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect owned bundle identifiers and directories so we don't capture `app` or `logger` by reference in the blocking thread.
|
|
||||||
let embedded_bundles_info: Vec<(String, PathBuf)> = app
|
|
||||||
.bundle
|
|
||||||
.embedded_bundles()
|
|
||||||
.iter()
|
|
||||||
.map(|bundle| {
|
|
||||||
(
|
|
||||||
bundle.bundle_identifier().unwrap_or("Unknown").to_string(),
|
|
||||||
bundle.bundle_dir.clone(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let main_bundle_dir = app.bundle.bundle_dir.clone();
|
|
||||||
|
|
||||||
// Log bundle signing messages outside the blocking closure to avoid capturing non-'static references.
|
|
||||||
for (id, _) in &embedded_bundles_info {
|
|
||||||
logger.log(&format!("Signing bundle: {}", id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move owned data (cert, provisioning_profile, embedded_bundles_info) into the blocking task.
|
|
||||||
tokio::task::spawn_blocking(move || {
|
|
||||||
for (_id, bundle_dir) in embedded_bundles_info {
|
|
||||||
// Recreate settings for each bundle so ownership is clear and we don't move settings across iterations.
|
|
||||||
let mut settings = cert.to_signing_settings()?;
|
|
||||||
settings
|
|
||||||
.set_entitlements_xml(
|
|
||||||
SettingsScope::Main,
|
|
||||||
provisioning_profile.entitlements_xml()?,
|
|
||||||
)
|
|
||||||
.map_err(|e| Error::AppleCodesignError(Box::new(e)))?;
|
|
||||||
|
|
||||||
let signer = UnifiedSigner::new(settings);
|
|
||||||
|
|
||||||
signer
|
|
||||||
.sign_path_in_place(&bundle_dir)
|
|
||||||
.map_err(|e| Error::AppleCodesignError(Box::new(e)))?;
|
|
||||||
}
|
|
||||||
Ok::<(), Error>(())
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|e| Error::Generic(format!("Signing task failed: {}", e)))??;
|
|
||||||
|
|
||||||
logger.log("Sucessfully signed app");
|
|
||||||
|
|
||||||
logger.log("Installing app... 0%");
|
|
||||||
|
|
||||||
let res = install_app(device_provider, &main_bundle_dir, |percentage| {
|
|
||||||
logger.log(&format!("Installing app... {}%", percentage));
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
if let Err(e) = res {
|
|
||||||
return error_and_return(logger, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if config.revoke_cert {
|
|
||||||
// dev_session
|
|
||||||
// .revoke_development_cert(DeveloperDeviceType::Ios, &team, &cert.get_serial_number()?)
|
|
||||||
// .await?;
|
|
||||||
// logger.log("Certificate revoked");
|
|
||||||
// }
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn ensure_device_registered(
|
|
||||||
logger: &dyn SideloadLogger,
|
|
||||||
dev_session: &DeveloperSession,
|
|
||||||
team: &DeveloperTeam,
|
|
||||||
uuid: &str,
|
|
||||||
name: &str,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let devices = dev_session
|
|
||||||
.list_devices(DeveloperDeviceType::Ios, team)
|
|
||||||
.await;
|
|
||||||
if let Err(e) = devices {
|
|
||||||
return error_and_return(logger, e);
|
|
||||||
}
|
|
||||||
let devices = devices.unwrap();
|
|
||||||
if !devices.iter().any(|d| d.device_number == uuid) {
|
|
||||||
logger.log("Device not found in your account");
|
|
||||||
// TODO: Actually test!
|
|
||||||
dev_session
|
|
||||||
.add_device(DeveloperDeviceType::Ios, team, name, uuid)
|
|
||||||
.await?;
|
|
||||||
logger.log("Successfully added device to your account");
|
|
||||||
}
|
|
||||||
logger.log("Device is a development device");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user