diff --git a/Common/Models/IviMachOBinary.cs b/Common/Models/IviMachOBinary.cs index 687a5b1..263e0d7 100644 --- a/Common/Models/IviMachOBinary.cs +++ b/Common/Models/IviMachOBinary.cs @@ -28,7 +28,6 @@ internal class IviMachOBinary(string fileName) return size switch { - < 1024 => $"{size:F0} bytes", _ when size >> 10 < 1024 => $"{size / (float)1024:F1} KB", _ when size >> 20 < 1024 => $"{(size >> 10) / (float)1024:F1} MB", @@ -43,7 +42,7 @@ internal class IviMachOBinary(string fileName) new ProcessStartInfo { FileName = "otool", - Arguments = $"-l \"{FullName}\"", + ArgumentList = { "-l", FullName }, RedirectStandardOutput = true } ); @@ -58,7 +57,7 @@ internal class IviMachOBinary(string fileName) new ProcessStartInfo { FileName = "lipo", - Arguments = $"-thin arm64 \"{FullName}\" -output \"{FullName}\"" + ArgumentList = { "-thin", "arm64", FullName, "-output", FullName } } ); diff --git a/Features/Codesigning/CodesigningMachOExtensions.cs b/Features/Codesigning/CodesigningMachOExtensions.cs index 44bdd2c..ad62ace 100644 --- a/Features/Codesigning/CodesigningMachOExtensions.cs +++ b/Features/Codesigning/CodesigningMachOExtensions.cs @@ -48,7 +48,7 @@ internal static class CodesigningMachOExtensions new ProcessStartInfo { FileName = "codesign", - Arguments = $"--remove-signature \"{binary.FullName}\"" + ArgumentList = { "--remove-signature", binary.FullName } } ); @@ -62,7 +62,7 @@ internal static class CodesigningMachOExtensions new ProcessStartInfo { FileName = "codesign", - Arguments = $"-d --entitlements {outputFilePath} --xml \"{binary.FullName}\"", + ArgumentList = { "-d", "--entitlements", outputFilePath, "--xml", binary.FullName }, RedirectStandardError = true } ); diff --git a/Features/Codesigning/CodesigningManager.cs b/Features/Codesigning/CodesigningManager.cs index d79ddbd..950eacd 100644 --- a/Features/Codesigning/CodesigningManager.cs +++ b/Features/Codesigning/CodesigningManager.cs @@ -195,6 +195,9 @@ internal class CodesigningManager(ILogger logger) : IDisposable if (!await SignBinary(MainBinary, signingInfo)) return false; + if (signingInfo.IsFromProvisioningProfile) + signingInfo.Entitlements!.Delete(); + logger.LogInformation( "Signed {} binaries ({} main executables) with the specified identity", _allBinaries.Count, diff --git a/Features/Command/IviRootCommand.cs b/Features/Command/IviRootCommand.cs index 4f6c966..120808f 100644 --- a/Features/Command/IviRootCommand.cs +++ b/Features/Command/IviRootCommand.cs @@ -50,6 +50,16 @@ internal class IviRootCommand : RootCommand AllowMultipleArgumentsPerToken = true }; + private readonly Option _provisioningProfileOption = new( + "--profile", + "Provisioning profile to extract entitlements, signing identity, and bundle ID" + ); + + private readonly Option _profileBundleIdOption = new( + "--profile-bundle-id", + "Replace the bundle ID with the one in the provisioning profile" + ); + private readonly Option _codesignIdentityOption = new( "--sign", "The identity for code signing (use \"-\" for ad hoc, a.k.a. fake signing)" @@ -63,7 +73,7 @@ internal class IviRootCommand : RootCommand // private readonly Option _customBundleIdOption = new( - "--bundleId", + "--bundle-id", "The custom identifier that will be applied to application bundles" ); @@ -86,9 +96,11 @@ internal class IviRootCommand : RootCommand internal IviRootCommand() : base("The most demure iOS app injector and signer") { _itemsOption.AddAlias("-i"); + + _provisioningProfileOption.AddAlias("-p"); _codesignIdentityOption.AddAlias("-s"); - _compressionLevelOption.AddAlias("--level"); _codesignEntitlementsOption.AddAlias("-e"); + _compressionLevelOption.AddAlias("--level"); _customBundleIdOption.AddAlias("-b"); _enableDocumentsSupportOption.AddAlias("-d"); @@ -101,6 +113,8 @@ internal class IviRootCommand : RootCommand AddOption(_compressionLevelOption); AddOption(_itemsOption); + AddOption(_provisioningProfileOption); + AddOption(_profileBundleIdOption); AddOption(_codesignIdentityOption); AddOption(_codesignEntitlementsOption); @@ -120,6 +134,8 @@ internal class IviRootCommand : RootCommand _overwriteOutputOption, _compressionLevelOption, _itemsOption, + _provisioningProfileOption, + _profileBundleIdOption, _codesignIdentityOption, _codesignEntitlementsOption, _customBundleIdOption, diff --git a/Features/Command/IviRootCommandParametersBinder.cs b/Features/Command/IviRootCommandParametersBinder.cs index 14bfa81..7b1fe97 100644 --- a/Features/Command/IviRootCommandParametersBinder.cs +++ b/Features/Command/IviRootCommandParametersBinder.cs @@ -12,6 +12,8 @@ internal class IviRootCommandParametersBinder( Option overwriteOutputOption, Option compressionLevelOption, Option> itemsOption, + Option provisioningProfileOption, + Option profileBundleIdOption, Option codesignIdentityOption, Option codesignEntitlementsOption, Option customBundleIdOption, @@ -33,12 +35,16 @@ internal class IviRootCommandParametersBinder( var items = bindingContext.ParseResult.GetValueForOption(itemsOption); + var provisioningProfile = + bindingContext.ParseResult.GetValueForOption(provisioningProfileOption); + var profileBundleId = + bindingContext.ParseResult.GetValueForOption(profileBundleIdOption); var codesignIdentity = bindingContext.ParseResult.GetValueForOption(codesignIdentityOption); var codesignEntitlements = bindingContext.ParseResult.GetValueForOption(codesignEntitlementsOption); - var bundleId = + var customBundleId = bindingContext.ParseResult.GetValueForOption(customBundleIdOption); var enableDocumentsSupport = bindingContext.ParseResult.GetValueForOption(enableDocumentsSupportOption); @@ -46,15 +52,43 @@ internal class IviRootCommandParametersBinder( bindingContext.ParseResult.GetValueForOption(removeSupportedDevicesOption); var directoriesToRemove = bindingContext.ParseResult.GetValueForOption(directoriesToRemoveOption); - - IviPackagingInfo? packagingInfo; - if (bundleId is null - && directoriesToRemove is null - && !enableDocumentsSupport - && !removeSupportedDevices) - packagingInfo = null; - else + IviSigningInfo? signingInfo = null; + IviPackagingInfo? packagingInfo = null; + + var profileInfo = provisioningProfile is not null + ? ProvisioningProfileParser.Parse(provisioningProfile) + : null; + + if (profileInfo is not null) + { + signingInfo = new IviSigningInfo + { + Identity = profileInfo.Identity, + Entitlements = profileInfo.Entitlements, + IsFromProvisioningProfile = true + }; + } + else if (codesignIdentity is not null) + { + signingInfo = new IviSigningInfo + { + Identity = codesignIdentity, + Entitlements = codesignEntitlements + }; + } + + var bundleId = customBundleId; + + if (profileInfo is not null && profileBundleId) + bundleId = profileInfo.BundleId; + + if (bundleId is not null + || directoriesToRemove is not null + || enableDocumentsSupport + || removeSupportedDevices + ) + { packagingInfo = new IviPackagingInfo { CustomBundleId = bundleId, @@ -62,6 +96,7 @@ internal class IviRootCommandParametersBinder( RemoveSupportedDevices = removeSupportedDevices, DirectoriesToRemove = directoriesToRemove ?? [] }; + } return new IviParameters { @@ -70,13 +105,7 @@ internal class IviRootCommandParametersBinder( OverwriteOutput = overwriteOutput, CompressionLevel = compressionLevel, InjectionEntries = items?.Select(item => new IviInjectionEntry(item)) ?? [], - SigningInfo = codesignIdentity is null - ? null - : new IviSigningInfo - { - Identity = codesignIdentity, - Entitlements = codesignEntitlements - }, + SigningInfo = signingInfo, PackagingInfo = packagingInfo }; } diff --git a/Features/Command/Models/IviSigningInfo.cs b/Features/Command/Models/IviSigningInfo.cs index c901f76..7c575ea 100644 --- a/Features/Command/Models/IviSigningInfo.cs +++ b/Features/Command/Models/IviSigningInfo.cs @@ -2,6 +2,7 @@ namespace ivinject.Features.Command.Models; internal class IviSigningInfo { + internal bool IsFromProvisioningProfile { get; init; } internal required string Identity { get; init; } internal bool IsAdHocSigning => Identity == "-"; internal FileInfo? Entitlements { get; init; } diff --git a/Features/Command/Models/ProfileInfo.cs b/Features/Command/Models/ProfileInfo.cs new file mode 100644 index 0000000..9500676 --- /dev/null +++ b/Features/Command/Models/ProfileInfo.cs @@ -0,0 +1,8 @@ +namespace ivinject.Features.Command.Models; + +internal class ProfileInfo +{ + internal required FileInfo Entitlements { get; init; } + internal required string Identity { get; set; } + internal required string BundleId { get; set; } +} \ No newline at end of file diff --git a/Features/Command/ProvisioningProfileParser.cs b/Features/Command/ProvisioningProfileParser.cs new file mode 100644 index 0000000..5f65e26 --- /dev/null +++ b/Features/Command/ProvisioningProfileParser.cs @@ -0,0 +1,39 @@ +using System.Diagnostics; +using Claunia.PropertyList; +using ivinject.Features.Command.Models; + +namespace ivinject.Features.Command; + +internal static class ProvisioningProfileParser +{ + internal static ProfileInfo Parse(FileInfo profile) + { + using var process = Process.Start( + new ProcessStartInfo + { + FileName = "security", + ArgumentList = { "cms", "-D", "-i", profile.FullName }, + RedirectStandardOutput = true + } + ); + + process!.WaitForExit(); + var dictionary = (NSDictionary)PropertyListParser.Parse(process.StandardOutput.BaseStream); + + var entitlements = (NSDictionary)dictionary["Entitlements"]; + var teamIdentifier = ((NSString)entitlements["com.apple.developer.team-identifier"]).Content; + var bundleId = ((NSString)entitlements["application-identifier"]).Content[11..]; + + var entitlementsFile = Path.GetTempFileName(); + File.WriteAllText(entitlementsFile, entitlements.ToXmlPropertyList()); + + var entitlementsFileInfo = new FileInfo(entitlementsFile); + + return new ProfileInfo + { + BundleId = bundleId, + Identity = teamIdentifier, + Entitlements = entitlementsFileInfo + }; + } +} \ No newline at end of file diff --git a/Features/Injection/DebManager.cs b/Features/Injection/DebManager.cs index 7a80548..d01118d 100644 --- a/Features/Injection/DebManager.cs +++ b/Features/Injection/DebManager.cs @@ -19,7 +19,7 @@ internal class DebManager(IviInjectionEntry debEntry) : IDisposable new ProcessStartInfo { FileName = "tar", - Arguments = $"-xf {debEntry.FullName} --directory={_tempPath}" + ArgumentList = { "-xf", debEntry.FullName, $"--directory={_tempPath}" } } ); @@ -31,7 +31,7 @@ internal class DebManager(IviInjectionEntry debEntry) : IDisposable new ProcessStartInfo { FileName = "tar", - Arguments = $"-xf {dataArchive}", + ArgumentList = { "-xf", dataArchive }, WorkingDirectory = _tempPath } ); diff --git a/Features/Injection/DependencyExtensions.cs b/Features/Injection/DependencyExtensions.cs index 5e87912..371d4c0 100644 --- a/Features/Injection/DependencyExtensions.cs +++ b/Features/Injection/DependencyExtensions.cs @@ -13,7 +13,7 @@ internal static class DependencyExtensions new ProcessStartInfo { FileName = "otool", - Arguments = $"-L \"{binary.FullName}\"", + ArgumentList = { "-L", binary.FullName }, RedirectStandardOutput = true } ); @@ -35,7 +35,7 @@ internal static class DependencyExtensions new ProcessStartInfo { FileName = "install_name_tool", - Arguments = $"-change {oldPath} {newPath} \"{binary.FullName}\"", + ArgumentList = { "-change", oldPath, newPath, binary.FullName }, RedirectStandardError = true } ); @@ -52,7 +52,7 @@ internal static class DependencyExtensions new ProcessStartInfo { FileName = "install_name_tool", - Arguments = $"-add_rpath {rPath} \"{binary.FullName}\"", + ArgumentList = { "-add_rpath", rPath, binary.FullName }, RedirectStandardOutput = true } ); @@ -70,7 +70,7 @@ internal static class DependencyExtensions new ProcessStartInfo { FileName = "insert-dylib", - Arguments = $"{dependency} \"{binary.FullName}\" --all-yes --inplace", + ArgumentList = { dependency, binary.FullName, "--all-yes", "--inplace" }, RedirectStandardOutput = true } ); diff --git a/Program.cs b/Program.cs index 6369a62..7d2faa5 100644 --- a/Program.cs +++ b/Program.cs @@ -3,7 +3,7 @@ using ivinject.Features.Command; namespace ivinject; -internal class Program +internal static class Program { private static readonly RootCommand RootCommand = new IviRootCommand(); diff --git a/README.md b/README.md index 7e447b8..b799307 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Thus, feature requests and bug reports are not accepted. You probably would like ## The Demureness - **ivinject is an entirely different project, written in C# with .NET 9. The code architecture and quality are significantly better. Compiled with NativeAOT, it produces native binaries, offering incredible speed and low resource usage, without needing anything like venv to run.** -- ivinject is not just an injector but also a signer. You can specify a code signing identity and a file with entitlements that will be written into the main executables. It signs the code properly according to Apple's technotes, passing codesign verification with the `--strict` option. It only supports developer certificates (.p12 and .mobileprovision). +- ivinject is not just an injector but also a signer. You can specify a code signing identity and a file with entitlements that will be written into the main executables. It signs the code properly according to Apple's technotes, passing codesign verification with the `--strict` option. It only supports already created provisioning profiles (you can specify the `.mobileprovision` file, or entitlements and identity manually). - ivinject does not and won’t support anything except for macOS — I couldn’t care less about other platforms. @@ -15,6 +15,6 @@ Thus, feature requests and bug reports are not accepted. You probably would like ## Prerequisites * Make sure Xcode is installed -* Install insert-dylib (`brew install --HEAD samdmarshall/formulae/insert-dylib`) +* Install insert-dylib (see `samdmarshall/formulae/insert-dylib`) * Copy the contents of `KnownFrameworks` to `~/.ivinject` -* For code signing, the identity needs to be added to Keychain, and the provisioning profile must be installed on the device (you can also add it to the app package by specifying `embedded.mobileprovision` in items) \ No newline at end of file +* For code signing, the identity needs to be added to Keychain, and the provisioning profile must be installed on the device (you can also add it to the app package by renaming it to `embedded.mobileprovision` and specifying it in the injection items) \ No newline at end of file