Skip to content

Commit c22d8ca

Browse files
committed
the basics for now
1 parent 04f5616 commit c22d8ca

16 files changed

Lines changed: 326 additions & 62 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ members = [
1010
edition = "2024"
1111
version = "0.0.1"
1212
authors = ["khcrysalis <s03781614@icloud.com>"]
13-
license = "GPL-3.0-or-later"
13+
license = "MIT"
1414
repository = "https://github.com/khcrysalis/plumestore"
1515

1616
[workspace.dependencies]

LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright 2025 Samara M
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a
4+
copy of this software and associated documentation files (the
5+
"Software"), to deal in the Software without restriction, including
6+
without limitation the rights to use, copy, modify, merge, publish,
7+
distribute, sublicense, and/or sell copies of the Software, and to
8+
permit persons to whom the Software is furnished to do so, subject to
9+
the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included
12+
in all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ The project is seperated in multiple modules, all serve single or multiple uses
1515
| `apps/plumeimpactor` | GUI interface for the crates shown below, backend using wxWidgets (with a rust ffi wrapper, wxDragon) |
1616
| `apps/plumesign` | CLI interface for the crates shown below, using `clap`. |
1717
| `crates/grand_slam` | Handles all api request used for communicating with Apple developer services, along with providing auth for Apple's grandslam |
18-
| `crates/ldid2` | Wrapper for applecodesign-rs with additional features, specifically made to support iOS sideloading and app modifications |
1918

2019
## Acknowledgements
2120

22-
- [Samara](https://github.com/khcrysalis) - ME!
23-
- [apple-private-apis](https://github.com/SideStore/apple-private-apis) - Grandslam auth & Omnisette.
24-
- [apple-codesign-rs](https://github.com/indygreg/apple-platform-rs) - Open-source alternative to codesign.
25-
- [idevice](https://github.com/jkcoxson/idevice) - Used for communication with `installd`, specifically for sideloading the apps to your devices.
21+
- [SAMSAM](https://github.com/khcrysalis) – The maker.
22+
- [SideStore](https://github.com/SideStore/apple-private-apis) – Grandslam auth & Omnisette.
23+
- [Sideloader](https://github.com/Dadoum/Sideloader) – Apple Developer API references.
24+
- [idevice](https://github.com/jkcoxson/idevice) – Used for communication with `installd`, specifically for sideloading the apps to your devices.
25+
- [apple-codesign-rs](https://github.com/indygreg/apple-platform-rs) – Open-source alternative to codesign.

apps/plumeimpactor/src/frame.rs

Lines changed: 143 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use std::cell::RefCell;
22
use std::path::PathBuf;
33
use std::rc::Rc;
4-
use std::{env, ptr, thread};
4+
use std::{env, fs, ptr, thread};
55

6-
use grand_slam::AnisetteConfiguration;
6+
use grand_slam::{AnisetteConfiguration, BundleType, MachO, MobileProvision};
77
use grand_slam::auth::Account;
88
use grand_slam::developer::DeveloperSession;
99
use grand_slam::utils::PlistInfoTrait;
@@ -311,12 +311,9 @@ impl PlumeFrame {
311311
let rt = Builder::new_current_thread().enable_all().build().unwrap();
312312

313313
let install_result = rt.block_on(async {
314-
let anisette_config = AnisetteConfiguration::default()
315-
.set_configuration_path(PathBuf::from(env::temp_dir()));
316-
317314
let session = DeveloperSession::with(account.clone());
318315

319-
sender_clone.send(PlumeFrameMessage::InstallProgress(0, Some("Ensuring device is registered...".to_string()))).ok();
316+
sender_clone.send(PlumeFrameMessage::InstallProgress(10, Some("Ensuring current device is registered...".to_string()))).ok();
320317

321318
let mut usbmuxd = UsbmuxdConnection::default().await
322319
.map_err(|e| format!("usbmuxd connect error: {e}"))?;
@@ -326,17 +323,151 @@ impl PlumeFrame {
326323
.find(|d| d.device_id.to_string() == device_id)
327324
.ok_or_else(|| format!("Device ID {device_id} not found"))?;
328325

329-
let mut lockdown = LockdownClient::connect(
330-
&usbmuxd_device.to_provider(UsbmuxdAddr::default(), "plume_install")
331-
)
332-
.await
333-
.map_err(|e| format!("lockdown connect error: {e}"))?;
326+
let device = Device::new(usbmuxd_device).await;
327+
328+
// TODO: Handle multiple teams properly
329+
let teams = session.qh_list_teams().await.map_err(|e| format!("Failed to list teams: {}", e))?;
330+
331+
session.qh_ensure_device(
332+
&teams.teams.get(0)
333+
.ok_or("No teams available for the Apple ID account.")?
334+
.team_id,
335+
&device.name,
336+
&device.uuid,
337+
).await.map_err(|e| format!("Failed to ensure device is registered: {}", e))?;
338+
339+
let bundle = package.get_package_bundle()
340+
.map_err(|e| format!("Failed to get package bundle: {}", e))?;
341+
let bundles = bundle.collect_bundles_sorted()
342+
.map_err(|e| format!("Failed to collect bundles: {}", e))?;
343+
344+
let team_id = &teams.teams.get(0)
345+
.ok_or("No teams available for the Apple ID account.")?
346+
.team_id;
347+
348+
let bundle_identifier = bundle.get_bundle_identifier()
349+
.ok_or("Failed to get bundle identifier from package.")?;
350+
351+
let new_id = new_identifier(&bundle_identifier, team_id);
352+
353+
fn new_identifier(original: &str, team_id: &str) -> String {
354+
format!("{}.{}", original, team_id)
355+
}
356+
357+
if let Some(old_identifier) = bundle.get_bundle_identifier() {
358+
for embedded_bundle in &bundles {
359+
embedded_bundle.set_matching_identifier(
360+
&old_identifier,
361+
&new_id,
362+
).map_err(|e| format!("Failed to set matching identifier: {}", e))?;
363+
}
364+
}
365+
366+
let mut provisionings: Vec<MobileProvision> = Vec::new();
334367

368+
for bundle in &bundles {
369+
if
370+
bundle._type != BundleType::AppExtension &&
371+
bundle._type != BundleType::App
372+
{
373+
continue;
374+
}
375+
376+
sender_clone.send(PlumeFrameMessage::InstallProgress(
377+
20,
378+
Some(format!("Registering {}...", bundle.get_name().unwrap_or_default()))
379+
)).ok();
380+
381+
let bundle_executable_name = bundle.get_executable()
382+
.ok_or("Failed to get executable from bundle.")?;
383+
384+
let bundle_executable_path = bundle.dir().join(&bundle_executable_name);
385+
386+
let macho = MachO::new(&bundle_executable_path)
387+
.map_err(|e| format!("Failed to read Mach-O binary: {}", e))?;
388+
389+
let macho_entitlements = macho.entitlements()
390+
.map_err(|e| format!("Failed to get entitlements from Mach-O binary: {}", e))?;
391+
392+
let id = bundle.get_bundle_identifier()
393+
.ok_or("Failed to get bundle identifier from bundle.")?;
394+
395+
let app_groups: Vec<String> = macho_entitlements
396+
.as_ref()
397+
.and_then(|dict| dict.get("com.apple.security.application-groups"))
398+
.and_then(|val| val.as_array())
399+
.map(|arr| {
400+
arr.iter()
401+
.filter_map(|v| v.as_string().map(|s| format!("{}.{}", s, team_id)))
402+
.collect()
403+
})
404+
.unwrap_or_else(Vec::new);
405+
406+
session.qh_ensure_app_id(team_id, &bundle.get_name().unwrap_or_default(), &id)
407+
.await
408+
.map_err(|e| format!("Failed to ensure app ID: {}", e))?;
409+
410+
let capabilities = session.v1_list_capabilities(team_id).await
411+
.map_err(|e| format!("Failed to list capabilities: {}", e))?;
412+
413+
println!("Mach-O Entitlements: {:?}", &macho_entitlements);
414+
415+
let mut capabilities_to_enable = Vec::new();
416+
417+
if let Some(entitlements) = &macho_entitlements {
418+
for (ent_key, _) in entitlements {
419+
for cap in &capabilities.data {
420+
if let Some(ent_list) = &cap.attributes.entitlements {
421+
if ent_list.iter().any(|e| e.profile_key == *ent_key) {
422+
capabilities_to_enable.push(cap.id.clone());
423+
}
424+
}
425+
}
426+
}
427+
}
428+
429+
println!("Enabling capabilities: {:?}", &capabilities_to_enable);
430+
431+
let app_id_id = session.qh_get_app_id(team_id, &id).await
432+
.map_err(|e| e.to_string())?
433+
.ok_or("Failed to get ensured app ID.")?;
434+
435+
if !capabilities_to_enable.is_empty() {
436+
session.v1_update_app_id(team_id, &id, capabilities_to_enable)
437+
.await
438+
.map_err(|e| format!("Failed to enable capabilities: {}", e))?;
439+
}
440+
441+
for group in &app_groups {
442+
let group_id = session.qh_ensure_app_group(team_id, group, group)
443+
.await
444+
.map_err(|e| format!("Failed to ensure app group: {}", e))?;
445+
446+
println!("{:#?}", group_id);
447+
448+
session.qh_assign_app_group(team_id, &app_id_id.app_id_id, &group_id.application_group)
449+
.await
450+
.map_err(|e| format!("Failed to add app group to app ID: {}", e))?;
451+
}
452+
453+
let profiles = session.qh_get_profile(team_id, &app_id_id.app_id_id).await
454+
.map_err(|e| format!("Failed to list profiles: {}", e))?;
455+
456+
let profile_data = profiles.provisioning_profile.encoded_profile;
457+
458+
let mobile_provision = MobileProvision::load_from_bytes(profile_data.as_ref())
459+
.map_err(|e| format!("Failed to load mobile provision: {}", e))?;
460+
461+
provisionings.push(mobile_provision);
462+
}
463+
464+
sender_clone.send(PlumeFrameMessage::InstallProgress(30, Some("Downloading Certificates...".to_string()))).ok();
465+
335466
Ok::<_, String>(())
336467
});
337468

338469
if let Err(e) = install_result {
339-
sender_clone.send(PlumeFrameMessage::InstallProgress(100, Some(format!("Install failed: {}", e)))).ok();
470+
sender_clone.send(PlumeFrameMessage::InstallProgress(99, Some(format!("{}", e)))).ok();
340471
return;
341472
}
342473
});

apps/plumeimpactor/src/handlers.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,12 @@ impl PlumeFrameMessageHandler {
180180
"Waiting...",
181181
100
182182
)
183-
.show_estimated_time().show_remaining_time().smooth().build();
183+
.show_elapsed_time()
184+
.show_estimated_time()
185+
.show_remaining_time()
186+
.smooth()
187+
.build();
188+
184189
self.installation_progress_dialog = Some(progress_dialog);
185190
}
186191

apps/plumeimpactor/src/pages/login.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,6 @@ impl AccountDialog {
140140
self.dialog.show_modal();
141141
}
142142

143-
pub fn hide(&self) {
144-
self.dialog.end_modal(0);
145-
}
146-
147143
pub fn set_logout_handler(&self, on_logout: impl Fn() + 'static) {
148144
let dialog = self.dialog.clone();
149145
self.logout_button.on_click(move |_| {

apps/plumeimpactor/src/utils/device.rs

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ use crate::Error;
88

99
pub const CONNECTION_LABEL: &str = "plume";
1010

11+
macro_rules! get_dict_string {
12+
($dict:expr, $key:expr) => {
13+
$dict
14+
.as_dictionary()
15+
.and_then(|dict| dict.get($key))
16+
.and_then(|v| v.as_string())
17+
.map(|s| s.to_string())
18+
.unwrap_or_else(|| "".to_string())
19+
};
20+
}
21+
1122
#[derive(Debug, Clone)]
1223
pub struct Device {
1324
pub name: String,
@@ -17,13 +28,24 @@ pub struct Device {
1728

1829
impl Device {
1930
pub async fn new(usbmuxd_device: UsbmuxdDevice) -> Self {
20-
let name = get_name_from_usbmuxd_device(&usbmuxd_device).await.unwrap_or_default();
21-
Device {
22-
name,
31+
let name = Self::get_name_from_usbmuxd_device(&usbmuxd_device)
32+
.await
33+
.unwrap_or_default();
34+
35+
Device {
36+
name,
2337
uuid: usbmuxd_device.udid.clone(),
2438
usbmuxd_device
2539
}
2640
}
41+
42+
async fn get_name_from_usbmuxd_device(
43+
device: &UsbmuxdDevice,
44+
) -> Result<String, Error> {
45+
let mut lockdown = LockdownClient::connect(&device.to_provider(UsbmuxdAddr::default(), CONNECTION_LABEL)).await?;
46+
let values = lockdown.get_value(None, None).await?;
47+
Ok(get_dict_string!(values, "DeviceName"))
48+
}
2749
}
2850

2951
impl fmt::Display for Device {
@@ -40,22 +62,3 @@ impl fmt::Display for Device {
4062
)
4163
}
4264
}
43-
44-
macro_rules! get_dict_string {
45-
($dict:expr, $key:expr) => {
46-
$dict
47-
.as_dictionary()
48-
.and_then(|dict| dict.get($key))
49-
.and_then(|v| v.as_string())
50-
.map(|s| s.to_string())
51-
.unwrap_or_else(|| "".to_string())
52-
};
53-
}
54-
55-
async fn get_name_from_usbmuxd_device(
56-
device: &UsbmuxdDevice,
57-
) -> Result<String, Error> {
58-
let mut lockdown = LockdownClient::connect(&device.to_provider(UsbmuxdAddr::default(), CONNECTION_LABEL)).await?;
59-
let values = lockdown.get_value(None, None).await?;
60-
Ok(get_dict_string!(values, "DeviceName"))
61-
}

crates/grand_slam/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "grand_slam"
33
edition.workspace = true
44
version.workspace = true
55
authors.workspace = true
6-
license.workspace = true
6+
license = "MPL-2.0"
77
repository.workspace = true
88

99
[package.metadata.patch]

crates/grand_slam/src/developer/qh/app_groups.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,26 @@ impl DeveloperSession {
3232

3333
Ok(response_data)
3434
}
35+
36+
pub async fn qh_get_app_group(&self, team_id: &str, app_group_identifier: &str) -> Result<Option<ApplicationGroup>, Error> {
37+
let response_data = self.qh_list_app_groups(team_id).await?;
38+
39+
let app_group = response_data.application_group_list.into_iter()
40+
.find(|group| group.identifier == app_group_identifier);
41+
42+
Ok(app_group)
43+
}
44+
45+
pub async fn qh_ensure_app_group(&self, team_id: &str, name: &str, identifier: &str) -> Result<ApplicationGroup, Error> {
46+
if let Some(app_group) = self.qh_get_app_group(team_id, identifier).await? {
47+
Ok(app_group)
48+
} else {
49+
let response = self.qh_add_app_group(team_id, name, identifier).await?;
50+
Ok(response.application_group)
51+
}
52+
}
3553

36-
pub async fn qh_assign_app_group(&self, team_id: &str, app_id_id: &str, app_group_id: &str) -> Result<AppGroupResponse, Error> {
54+
pub async fn qh_assign_app_group(&self, team_id: &str, app_id_id: &str, app_group_id: &str) -> Result<ResponseMeta, Error> {
3755
let endpoint = developer_endpoint!("/QH65B2/ios/assignApplicationGroupToAppId.action");
3856

3957
let mut body = Dictionary::new();
@@ -42,7 +60,7 @@ impl DeveloperSession {
4260
body.insert("applicationGroups".to_string(), Value::String(app_group_id.to_string()));
4361

4462
let response = self.qh_send_request(&endpoint, Some(body)).await?;
45-
let response_data: AppGroupResponse = plist::from_value(&Value::Dictionary(response))?;
63+
let response_data: ResponseMeta = plist::from_value(&Value::Dictionary(response))?;
4664

4765
Ok(response_data)
4866
}
@@ -70,9 +88,9 @@ pub struct AppGroupResponse {
7088
#[derive(Deserialize, Debug)]
7189
#[serde(rename_all = "camelCase")]
7290
pub struct ApplicationGroup {
73-
pub application_group: String,
91+
pub application_group: String, // this is the actual identifier
7492
pub name: String,
7593
pub status: String,
7694
prefix: String,
77-
pub identifier: String,
95+
pub identifier: String, // this is the group.identifier
7896
}

0 commit comments

Comments
 (0)