use super::{AppleSimulatorType, SignatureSettings, SigningIdentity};
use crate::errors::*;
use fs_err as fs;
use log::{debug, trace};
use std::io::Write;
use std::{io, process};
use crate::utils::LogCommandExt;
use crate::BuildBundle;
pub fn add_plist_to_app(
bundle: &BuildBundle,
arch: &str,
app_bundle_id: &str,
sim_type: Option<&AppleSimulatorType>,
) -> Result<()> {
let mut plist = fs::File::create(bundle.bundle_dir.join("Info.plist"))?;
writeln!(plist, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
writeln!(
plist,
r#"<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">"#
)?;
writeln!(plist, r#"<plist version="1.0"><dict>"#)?;
writeln!(plist, "<key>CFBundleName</key><string>Dinghy</string>",)?;
writeln!(
plist,
"<key>CFBundleExecutable</key><string>Dinghy</string>",
)?;
writeln!(
plist,
"<key>CFBundleIdentifier</key><string>{}</string>",
app_bundle_id
)?;
writeln!(plist, "<key>CFBundleVersion</key>")?;
writeln!(plist, "<string>{}</string>", arch)?;
writeln!(plist, "<key>CFBundleShortVersionString</key>")?;
writeln!(plist, "<string>{}</string>", arch)?;
match sim_type {
Some(AppleSimulatorType::Ios) | Some(AppleSimulatorType::Tvos) | None => {
writeln!(plist, "<key>UIRequiredDeviceCapabilities</key>")?;
writeln!(plist, "<array><string>{}</string></array>", arch)?;
writeln!(plist, "<key>UILaunchStoryboardName</key>")?;
writeln!(plist, "<string></string>")?;
}
Some(AppleSimulatorType::Watchos) => {
writeln!(plist, "<key>MinimumOSVersion</key><string>8.0</string>",)?;
writeln!(plist, "<key>WKApplication</key><true/>",)?;
writeln!(plist, "<key>WKWatchOnly</key><true/>")?;
}
}
writeln!(plist, r#"</dict></plist>"#)?;
Ok(())
}
pub fn sign_app(bundle: &BuildBundle, settings: &SignatureSettings) -> Result<()> {
debug!(
"Will sign {:?} with team: {} using key: {} and profile: {}",
bundle.bundle_dir, settings.identity.team, settings.identity.name, settings.file
);
let entitlements = bundle.root_dir.join("entitlements.xcent");
debug!("entitlements file: {}", entitlements.to_str().unwrap_or(""));
let mut plist = fs::File::create(&entitlements)?;
writeln!(plist, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
writeln!(
plist,
r#"<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">"#
)?;
writeln!(plist, r#"<plist version="1.0"><dict>"#)?;
writeln!(plist, "{}", settings.entitlements)?;
writeln!(plist, r#"</dict></plist>"#)?;
let result = process::Command::new("codesign")
.args(&["-s", &*settings.identity.name, "--entitlements"])
.arg(entitlements)
.arg(&bundle.bundle_dir)
.log_invocation(2)
.status()?;
if !result.success() {
bail!("Failure to sign application: codesign utility returned non-zero");
}
Ok(())
}
pub fn look_for_signature_settings(device_id: &str) -> Result<Vec<SignatureSettings>> {
let identity_regex = ::regex::Regex::new(r#"^ *[0-9]+\) ([A-Z0-9]{40}) "(.+)"$"#)?;
let subject_regex = ::regex::Regex::new(r#"OU *= *([^,]+)"#)?;
let mut identities: Vec<SigningIdentity> = vec![];
let find_identities = process::Command::new("security")
.args(&["find-identity", "-v", "-p", "codesigning"])
.log_invocation(3)
.output()?;
for line in String::from_utf8(find_identities.stdout)?.split("\n") {
if let Some(caps) = identity_regex.captures(&line) {
let name: String = caps[2].into();
if !name.starts_with("iPhone Developer: ") && !name.starts_with("Apple Development:") {
continue;
}
let subject = process::Command::new("sh")
.arg("-c")
.arg(format!(
"security find-certificate -a -c \"{}\" -p | openssl x509 -text | \
grep Subject:",
name
))
.log_invocation(3)
.output()?;
let subject = String::from_utf8(subject.stdout)?;
if let Some(ou) = subject_regex.captures(&subject) {
identities.push(SigningIdentity {
id: caps[1].into(),
name: caps[2].into(),
team: ou[1].into(),
})
}
}
}
debug!("Possible signing identities: {:?}", identities);
let mut settings = vec![];
let home = dirs::home_dir().expect("can't get HOME dir");
let profiles_dir = [
"Library/MobileDevice/Provisioning Profiles",
"Library/Developer/Xcode/UserData/Provisioning Profiles", ];
for file in profiles_dir
.iter()
.filter_map(|path| fs::read_dir(home.join(path)).ok())
.flatten()
{
let file = file?;
if file.path().starts_with(".")
|| file
.path()
.extension()
.map(|ext| ext.to_string_lossy() != "mobileprovision")
.unwrap_or(true)
{
trace!(
" - skipping {:?} (not a mobileprovision profile)",
file.path()
);
continue;
}
let decoded = process::Command::new("security")
.arg("cms")
.arg("-D")
.arg("-i")
.arg(file.path())
.log_invocation(3)
.output()?;
let plist = plist::Value::from_reader(io::Cursor::new(&decoded.stdout))
.with_context(|| format!("While reading profile {:?}", file.path()))?;
let dict = plist
.as_dictionary()
.ok_or_else(|| anyhow!("plist root should be a dictionary"))?;
let devices = if let Some(d) = dict.get("ProvisionedDevices") {
d
} else {
trace!(" - skipping {:?} (no devices)", file.path());
continue;
};
let devices = if let Some(ds) = devices.as_array() {
ds
} else {
bail!("ProvisionedDevices expected to be array")
};
if !devices.contains(&plist::Value::String(device_id.into())) {
trace!(" - skipping {:?} (not matching target device)", file.path());
continue;
}
let name = dict
.get("Name")
.ok_or_else(|| anyhow!(format!("No name in profile {:?}", file.path())))?;
let name = name
.as_string()
.ok_or_else(|| anyhow!("Name should have been a string in {:?}", file.path()))?;
if !name.ends_with("Dinghy") && !name.ends_with(" *") {
trace!(" - skipping {:?} (wrong app)", file.path());
continue;
}
let team = dict
.get("TeamIdentifier")
.ok_or_else(|| anyhow!("no TeamIdentifier"))?;
let team = team
.as_array()
.ok_or_else(|| anyhow!("TeamIdentifier should be an array"))?;
let team = team
.first()
.ok_or_else(|| anyhow!("empty TeamIdentifier"))?
.as_string()
.ok_or_else(|| anyhow!("TeamIdentifier should be a String"))?
.to_string();
let identity = identities.iter().find(|i| i.team == team);
if identity.is_none() {
trace!(
" - skipping {:?} (no identity in profile for team)",
file.path()
);
continue;
}
let identity = identity.unwrap();
trace!(" - accepting {:?}", file.path());
let entitlements = String::from_utf8(decoded.stdout)?
.split("\n")
.skip_while(|line| !line.contains("<key>Entitlements</key>"))
.skip(2)
.take_while(|line| !line.contains("</dict>"))
.collect::<Vec<&str>>()
.join("\n");
settings.push(SignatureSettings {
entitlements: entitlements,
file: file
.path()
.to_str()
.ok_or_else(|| anyhow!("filename should be utf8"))?
.into(),
name: if name.ends_with(" *") {
"org.zoy.kali.Dinghy".into()
} else {
name.into()
},
identity: identity.clone(),
profile: file.path().to_str().unwrap().into(),
});
}
Ok(settings)
}