160 lines
7.2 KiB
Diff
160 lines
7.2 KiB
Diff
diff --git a/isideload/src/sideload/application.rs b/isideload/src/sideload/application.rs
|
|
index 4e0ccd0..be80af4 100644
|
|
--- a/isideload/src/sideload/application.rs
|
|
+++ b/isideload/src/sideload/application.rs
|
|
@@ -20,6 +20,33 @@ pub struct Application {
|
|
//pub temp_path: PathBuf,
|
|
}
|
|
|
|
+#[cfg(test)]
|
|
+mod musicbridge_tests {
|
|
+ use super::*;
|
|
+ #[test]
|
|
+ fn musicbridge_extensionkit_is_renamed_and_persisted() {
|
|
+ let root = std::env::temp_dir().join(format!("musicbridge-sign-test-{}", uuid::Uuid::new_v4()));
|
|
+ for (directory, id) in [("", "ru.yukinoki.musicbridge"),
|
|
+ ("PlugIns/Widget.appex", "ru.yukinoki.musicbridge.widgets"),
|
|
+ ("Extensions/Remote.appex", "ru.yukinoki.musicbridge.remote")] {
|
|
+ let path = root.join(directory);
|
|
+ std::fs::create_dir_all(&path).unwrap();
|
|
+ let mut info = plist::Dictionary::new();
|
|
+ info.insert("CFBundleIdentifier".into(), id.into());
|
|
+ plist::to_file_xml(path.join("Info.plist"), &info).unwrap();
|
|
+ }
|
|
+ let mut app = Application::new(root.clone()).unwrap();
|
|
+ assert_eq!(app.bundle.app_extensions().len(), 2);
|
|
+ app.update_bundle_id("ru.yukinoki.musicbridge", "ru.yukinoki.musicbridge.TEAM").unwrap();
|
|
+ app.bundle.write_info().unwrap();
|
|
+ for extension in app.bundle.app_extensions_mut() { extension.write_info().unwrap(); }
|
|
+ let saved = Application::new(root).unwrap();
|
|
+ for extension in saved.bundle.app_extensions() {
|
|
+ assert!(extension.bundle_identifier().unwrap().starts_with("ru.yukinoki.musicbridge.TEAM."));
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
impl Application {
|
|
pub fn new(path: PathBuf) -> Result<Self, Report> {
|
|
if !isideload_vfs::fs::metadata(&path).is_ok() {
|
|
diff --git a/isideload/src/sideload/bundle.rs b/isideload/src/sideload/bundle.rs
|
|
index 67a947d..2cc2282 100644
|
|
--- a/isideload/src/sideload/bundle.rs
|
|
+++ b/isideload/src/sideload/bundle.rs
|
|
@@ -45,23 +45,19 @@ impl Bundle {
|
|
"Failed to parse Info.plist".to_string(),
|
|
))?;
|
|
|
|
- // Load app extensions from PlugIns directory
|
|
- let plug_ins_dir = bundle_path.join("PlugIns");
|
|
- let app_extensions = if isideload_vfs::fs::metadata(&plug_ins_dir).is_ok() {
|
|
- isideload_vfs::fs::read_dir(&plug_ins_dir)
|
|
- .context(SideloadError::InvalidBundle(
|
|
- "Failed to read PlugIns directory".to_string(),
|
|
- ))?
|
|
- .filter_map(|entry| entry.ok())
|
|
- .filter(|entry| {
|
|
- entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
|
|
- && isideload_vfs::fs::metadata(&entry.path().join("Info.plist")).is_ok()
|
|
- })
|
|
- .filter_map(|entry| Bundle::new(entry.path()).ok())
|
|
- .collect()
|
|
- } else {
|
|
- Vec::new()
|
|
- };
|
|
+ // ExtensionKit uses Extensions; WidgetKit and older extensions use PlugIns.
|
|
+ // Invalid nested bundles must fail signing, never be silently skipped.
|
|
+ let mut app_extensions = Vec::new();
|
|
+ for folder in ["PlugIns", "Extensions"] {
|
|
+ let directory = bundle_path.join(folder);
|
|
+ if !isideload_vfs::fs::metadata(&directory).is_ok() { continue; }
|
|
+ for entry in isideload_vfs::fs::read_dir(&directory)? {
|
|
+ let entry = entry?;
|
|
+ if entry.file_type()?.is_dir() {
|
|
+ app_extensions.push(Bundle::new(entry.path())?);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
|
|
// Load frameworks from Frameworks directory
|
|
let frameworks_dir = bundle_path.join("Frameworks");
|
|
diff --git a/isideload/src/sideload/sign.rs b/isideload/src/sideload/sign.rs
|
|
index a3d9799..c7e6154 100644
|
|
--- a/isideload/src/sideload/sign.rs
|
|
+++ b/isideload/src/sideload/sign.rs
|
|
@@ -5,6 +5,7 @@ use apple_codesign::{
|
|
};
|
|
|
|
use plist::Dictionary;
|
|
+use rootcause::option_ext::OptionExt;
|
|
use rootcause::prelude::*;
|
|
|
|
use crate::{
|
|
@@ -63,6 +64,10 @@ where
|
|
);
|
|
}
|
|
|
|
+ let musicbridge = app.main_bundle_id()? == format!("ru.yukinoki.musicbridge.{}", team.team_id)
|
|
+ && app.bundle.app_info.get("MusicBridgeSharedKeychainGroup").and_then(plist::Value::as_string)
|
|
+ == Some("ru.yukinoki.musicbridge.remote-control");
|
|
+ if musicbridge { entitlements = musicbridge_keychain(&entitlements)?; }
|
|
let mut settings = BundleSigningSettings::new(&team.team_id, entitlements, Some(&signer));
|
|
settings.embedded_mobileprovision = Some(main_provisioning_profile.encoded_profile.as_ref());
|
|
|
|
@@ -75,9 +80,56 @@ where
|
|
.map(|(bundle_id, _, entitlements)| (bundle_id.clone(), entitlements.clone()))
|
|
.collect();
|
|
|
|
+ if musicbridge {
|
|
+ let remote_id = format!("{}.remote", app.main_bundle_id()?);
|
|
+ let remote = settings.entitlements_by_bundle_id.get(&remote_id)
|
|
+ .ok_or_report().context("MusicBridge remote extension has no provisioning profile")?;
|
|
+ let remote = musicbridge_keychain(remote)?;
|
|
+ settings.entitlements_by_bundle_id.insert(remote_id, remote);
|
|
+ }
|
|
+
|
|
if let Some(callback) = &progress_callback {
|
|
callback(0.5).await;
|
|
}
|
|
|
|
Ok(sign_bundle(&app.bundle.bundle_dir, &settings)?)
|
|
}
|
|
+
|
|
+// Request only a group explicitly permitted by Apple's actual profile.
|
|
+fn musicbridge_keychain(profile: &Dictionary) -> Result<Dictionary, Report> {
|
|
+ let application = profile.get("application-identifier").and_then(plist::Value::as_string)
|
|
+ .ok_or_report().context("Profile has no application-identifier")?;
|
|
+ let prefix = application.split_once('.').ok_or_report().context("Invalid app identifier")?.0;
|
|
+ let shared = format!("{}.ru.yukinoki.musicbridge.remote-control", prefix);
|
|
+ let allowed = profile.get("keychain-access-groups").and_then(plist::Value::as_array)
|
|
+ .ok_or_report().context("Profile has no Keychain groups")?;
|
|
+ if !allowed.iter().filter_map(plist::Value::as_string).any(|group|
|
|
+ group == shared || group == format!("{}.*", prefix)) {
|
|
+ bail!("Apple's provisioning profile does not permit the MusicBridge shared Keychain group");
|
|
+ }
|
|
+ let mut result = profile.clone();
|
|
+ // A concrete per-app default group comes first, shared group second.
|
|
+ result.insert("keychain-access-groups".into(), plist::Value::Array(vec![
|
|
+ plist::Value::String(application.into()), plist::Value::String(shared)]));
|
|
+ Ok(result)
|
|
+}
|
|
+
|
|
+#[cfg(test)]
|
|
+mod musicbridge_tests {
|
|
+ use super::*;
|
|
+ fn profile(group: &str) -> Dictionary {
|
|
+ let mut p = Dictionary::new();
|
|
+ p.insert("application-identifier".into(), "TEAM.ru.yukinoki.musicbridge.TEAM".into());
|
|
+ p.insert("keychain-access-groups".into(), plist::Value::Array(vec![group.into()]));
|
|
+ p
|
|
+ }
|
|
+ #[test]
|
|
+ fn musicbridge_sharing_respects_profile() {
|
|
+ let value = musicbridge_keychain(&profile("TEAM.*")).unwrap();
|
|
+ let groups = value["keychain-access-groups"].as_array().unwrap();
|
|
+ assert_eq!(groups[0].as_string(), Some("TEAM.ru.yukinoki.musicbridge.TEAM"));
|
|
+ assert_eq!(groups[1].as_string(), Some("TEAM.ru.yukinoki.musicbridge.remote-control"));
|
|
+ assert!(musicbridge_keychain(&profile("OTHER.*")).is_err());
|
|
+ assert!(musicbridge_keychain(&profile("TEAM.unrelated")).is_err());
|
|
+ }
|
|
+}
|