6 Commits
first ... main

Author SHA1 Message Date
eevee
72213ba3cf 10 2025-09-14 02:02:05 +03:00
eevee
793bad1261 migration to System.CommandLine 2.0.0-beta5+ 2025-09-14 01:59:57 +03:00
eevee
68722c2084 provisioning profiles support 2025-09-14 01:24:47 +03:00
eevee
5f4adec22c merging entitlements 2025-05-04 17:44:53 +03:00
eevee
661619ccd7 one more escape 2025-03-30 15:03:11 +03:00
eevee
1c3b7ed4f2 escaping paths with spaces, updated ellekit 2025-03-30 14:54:36 +03:00
18 changed files with 392 additions and 237 deletions

View File

@@ -28,7 +28,6 @@ internal class IviMachOBinary(string fileName)
return size switch return size switch
{ {
< 1024 => $"{size:F0} bytes", < 1024 => $"{size:F0} bytes",
_ when size >> 10 < 1024 => $"{size / (float)1024:F1} KB", _ when size >> 10 < 1024 => $"{size / (float)1024:F1} KB",
_ when size >> 20 < 1024 => $"{(size >> 10) / (float)1024:F1} MB", _ when size >> 20 < 1024 => $"{(size >> 10) / (float)1024:F1} MB",
@@ -43,7 +42,7 @@ internal class IviMachOBinary(string fileName)
new ProcessStartInfo new ProcessStartInfo
{ {
FileName = "otool", FileName = "otool",
Arguments = $"-l {FullName}", ArgumentList = { "-l", FullName },
RedirectStandardOutput = true RedirectStandardOutput = true
} }
); );
@@ -58,7 +57,7 @@ internal class IviMachOBinary(string fileName)
new ProcessStartInfo new ProcessStartInfo
{ {
FileName = "lipo", FileName = "lipo",
Arguments = $"-thin arm64 {FullName} -output {FullName}" ArgumentList = { "-thin", "arm64", FullName, "-output", FullName }
} }
); );

View File

@@ -1,30 +1,33 @@
using System.Diagnostics; using System.Diagnostics;
using System.Text; using System.Text;
using ivinject.Common.Models; using ivinject.Common.Models;
using ivinject.Features.Packaging.Models;
using static ivinject.Features.Packaging.Models.DirectoryNames;
namespace ivinject.Features.Codesigning; namespace ivinject.Features.Codesigning;
internal static class CodesigningMachOExtensions internal static class CodesigningMachOExtensions
{ {
internal static bool IsMainExecutable(this IviMachOBinary binary, IviDirectoriesInfo directoriesInfo)
{
return !Path.GetRelativePath(
directoriesInfo.BundleDirectory,
binary.FullName
).Contains(FrameworksDirectoryName);
}
internal static async Task<bool> SignAsync( internal static async Task<bool> SignAsync(
this IviMachOBinary binary, this IviMachOBinary binary,
string identity, string identity,
bool force, FileInfo? entitlements = null
FileInfo? entitlements,
bool preserveEntitlements = false
) )
{ {
var arguments = new StringBuilder($"-s {identity}"); var arguments = new StringBuilder($"-s {identity}");
if (entitlements is not null) if (entitlements is not null)
arguments.Append($" --entitlements {entitlements.FullName}"); arguments.Append($" --entitlements {entitlements.FullName}");
else if (preserveEntitlements)
arguments.Append(" --preserve-metadata=entitlements");
if (force) arguments.Append($" \"{binary.FullName}\"");
arguments.Append(" -f");
arguments.Append($" {binary.FullName}");
using var process = Process.Start( using var process = Process.Start(
new ProcessStartInfo new ProcessStartInfo
@@ -45,7 +48,7 @@ internal static class CodesigningMachOExtensions
new ProcessStartInfo new ProcessStartInfo
{ {
FileName = "codesign", FileName = "codesign",
Arguments = $"--remove-signature {binary.FullName}" ArgumentList = { "--remove-signature", binary.FullName }
} }
); );
@@ -59,12 +62,14 @@ internal static class CodesigningMachOExtensions
new ProcessStartInfo new ProcessStartInfo
{ {
FileName = "codesign", FileName = "codesign",
Arguments = $"-d --entitlements {outputFilePath} --xml {binary.FullName}", ArgumentList = { "-d", "--entitlements", outputFilePath, "--xml", binary.FullName },
RedirectStandardError = true RedirectStandardError = true
} }
); );
await process!.WaitForExitAsync(); var error = await process!.StandardError.ReadToEndAsync();
return process.ExitCode == 0; await process.WaitForExitAsync();
return process.ExitCode == 0 && error.Count(c => c.Equals('\n')) == 1;
} }
} }

View File

@@ -1,9 +1,12 @@
using System.Collections.Concurrent;
using Claunia.PropertyList;
using ivinject.Common; using ivinject.Common;
using ivinject.Common.Models; using ivinject.Common.Models;
using ivinject.Features.Codesigning.Models; using ivinject.Features.Codesigning.Models;
using ivinject.Features.Command.Models;
using ivinject.Features.Packaging;
using ivinject.Features.Packaging.Models; using ivinject.Features.Packaging.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static ivinject.Features.Packaging.Models.DirectoryNames;
using static ivinject.Common.Models.BinaryHeaders; using static ivinject.Common.Models.BinaryHeaders;
namespace ivinject.Features.Codesigning; namespace ivinject.Features.Codesigning;
@@ -11,12 +14,20 @@ namespace ivinject.Features.Codesigning;
internal class CodesigningManager(ILogger logger) : IDisposable internal class CodesigningManager(ILogger logger) : IDisposable
{ {
private IviPackageInfo _packageInfo = null!; private IviPackageInfo _packageInfo = null!;
private IviDirectoriesInfo DirectoriesInfo => _packageInfo.DirectoriesInfo;
//
private readonly List<IviMachOBinary> _allBinaries = []; private readonly List<IviMachOBinary> _allBinaries = [];
private List<IviMachOBinary> Binaries => _allBinaries[1..];
private IviMachOBinary MainBinary => _allBinaries[0]; private IviMachOBinary MainBinary => _allBinaries[0];
private List<IviMachOBinary> Binaries => _allBinaries[1..];
private List<IviMachOBinary> MainExecutables =>
_allBinaries.Where(binary => binary.IsMainExecutable(DirectoriesInfo)).ToList();
private FileInfo? _savedMainEntitlements; //
private readonly ConcurrentDictionary<IviMachOBinary, FileInfo> _savedEntitlements = [];
private readonly List<FileInfo> _mergedEntitlementFiles = [];
internal void UpdateWithPackage(IviPackageInfo packageInfo) internal void UpdateWithPackage(IviPackageInfo packageInfo)
{ {
@@ -73,87 +84,148 @@ internal class CodesigningManager(ILogger logger) : IDisposable
}; };
} }
internal async Task SaveMainBinaryEntitlementsAsync() internal async Task<bool> SaveMainExecutablesEntitlementsAsync()
{ {
var tempFile = Path.GetTempFileName(); var dumpingResults = await Task.WhenAll(
MainExecutables.Select(async binary =>
if (await MainBinary.DumpEntitlementsAsync(tempFile))
{
logger.LogInformation("Saved {} entitlements", MainBinary.Name);
_savedMainEntitlements = new FileInfo(tempFile);
return;
}
logger.LogWarning(
"Unable to save {} entitlements. The binary is likely unsigned.",
MainBinary.Name
);
}
internal async Task<bool> SignAsync(string identity, bool isAdHocSigning, FileInfo? entitlements)
{
var mainExecutablesCount = 0;
var signingResults = await Task.WhenAll(
Binaries.Select(async binary =>
{ {
var isMainExecutable = !Path.GetRelativePath( var tempFile = Path.GetTempFileName();
_packageInfo.DirectoriesInfo.BundleDirectory, var tempFileInfo = new FileInfo(tempFile);
binary.FullName
).Contains(FrameworksDirectoryName);
if (isMainExecutable) if (!await binary.DumpEntitlementsAsync(tempFile))
mainExecutablesCount++; {
tempFileInfo.Delete();
return false;
}
return await binary.SignAsync( _savedEntitlements[binary] = tempFileInfo;
identity, return true;
isAdHocSigning,
isMainExecutable
? entitlements
: null,
isMainExecutable && entitlements is null
);
} }
) )
); );
return dumpingResults.All(result => result);
}
private async Task<FileInfo> MergeEntitlementsAsync(
string binaryName,
FileInfo binaryEntitlements,
FileInfo profileEntitlements
)
{
var binaryEntitlementsDictionary = (NSDictionary)PropertyListParser.Parse(binaryEntitlements);
var profileEntitlementsDictionary = (NSDictionary)PropertyListParser.Parse(profileEntitlements);
var profileTeamId = ((NSString)profileEntitlementsDictionary["com.apple.developer.team-identifier"]).Content;
var finalEntitlements = new NSDictionary();
foreach (var entitlementKey in binaryEntitlementsDictionary.Keys)
{
if (!profileEntitlementsDictionary.TryGetValue(entitlementKey, out var profileEntitlement))
{
logger.LogWarning(
"Matching entitlement for {} ({}) was not found",
entitlementKey,
binaryName
);
continue;
}
if (entitlementKey == "keychain-access-groups")
{
var binaryAccessGroups = (NSArray)binaryEntitlementsDictionary[entitlementKey];
var finalAccessGroups = new NSArray();
foreach (var accessGroup in binaryAccessGroups)
{
var group = profileTeamId + ((NSString)accessGroup).Content[10..];
finalAccessGroups.Add(new NSString(group));
}
finalEntitlements[entitlementKey] = finalAccessGroups;
logger.LogInformation("Mapped keychain access groups for {}: {}", binaryName, finalAccessGroups);
continue;
}
finalEntitlements[entitlementKey] = profileEntitlement;
}
var finalEntitlementsPath = Path.GetTempFileName();
await finalEntitlements.SaveToFileAsync(finalEntitlementsPath);
var fileInfo = new FileInfo(finalEntitlementsPath);
_mergedEntitlementFiles.Add(fileInfo);
return fileInfo;
}
private async Task<bool> SignBinary(IviMachOBinary binary, IviSigningInfo signingInfo)
{
var identity = signingInfo.Identity;
var entitlements = signingInfo.Entitlements;
if (!binary.IsMainExecutable(DirectoriesInfo))
return await binary.SignAsync(identity);
if (!_savedEntitlements.TryGetValue(binary, out var signingEntitlements))
return await binary.SignAsync(identity, entitlements);
if (!signingInfo.IsAdHocSigning)
signingEntitlements = await MergeEntitlementsAsync(
binary.Name,
signingEntitlements,
entitlements!
);
return await binary.SignAsync(identity, signingEntitlements);
}
internal async Task<bool> SignPackageAsync(IviSigningInfo signingInfo)
{
var signingResults = await Task.WhenAll(
Binaries.Select(async binary => await SignBinary(binary, signingInfo))
);
if (signingResults.Any(result => !result)) if (signingResults.Any(result => !result))
return false; return false;
if (!await MainBinary.SignAsync(identity, false, _savedMainEntitlements ?? entitlements)) if (!await SignBinary(MainBinary, signingInfo))
return false; return false;
if (signingInfo.IsFromProvisioningProfile)
signingInfo.Entitlements!.Delete();
logger.LogInformation( logger.LogInformation(
"Signed {} binaries ({} main executables) with the specified identity", "Signed {} binaries ({} main executables) with the specified identity",
_allBinaries.Count, _allBinaries.Count,
mainExecutablesCount + 1 // App main executable MainExecutables.Count
); );
return true; return true;
} }
internal async Task<bool> RemoveSignatureAsync(bool allBinaries) internal async Task<bool> RemoveSignatureAsync()
{ {
if (allBinaries) var removingResults = await Task.WhenAll(
{ _allBinaries.Select(binary => binary.RemoveSignatureAsync())
var removingResults = await Task.WhenAll( );
_allBinaries.Select(binary => binary.RemoveSignatureAsync())
);
if (removingResults.Any(result => !result)) if (removingResults.Any(result => !result))
return false;
logger.LogInformation("Signature removed from {} binaries", _allBinaries.Count);
return true;
}
if (!await MainBinary.RemoveSignatureAsync())
return false; return false;
logger.LogInformation("Removed {} signature", MainBinary.Name); logger.LogInformation("Signature removed from {} binaries", _allBinaries.Count);
return true; return true;
} }
public void Dispose() => _savedMainEntitlements?.Delete(); public void Dispose()
{
foreach (var fileInfo in _savedEntitlements.Values)
fileInfo.Delete();
foreach (var fileInfo in _mergedEntitlementFiles)
fileInfo.Delete();
}
} }

View File

@@ -12,122 +12,135 @@ internal class IviRootCommand : RootCommand
var value = result.Tokens[0].Value; var value = result.Tokens[0].Value;
if (!RegularExpressions.ApplicationPackage().IsMatch(value)) if (!RegularExpressions.ApplicationPackage().IsMatch(value))
result.ErrorMessage = "The application package must be either an .app bundle or an .ipa$ archive."; result.AddError("The application package must be either an .app bundle or an .ipa$ archive.");
return value; return value;
} }
// //
private readonly Argument<string> _targetArgument = new( private readonly Argument<string> _targetArgument = new("target")
name: "target", {
description: "The application package, either .app bundle or ipa$", Description = "The application package, either .app bundle or ipa$",
parse: ParseAppPackageResult CustomParser = ParseAppPackageResult
); };
private readonly Argument<string> _outputArgument = new( private readonly Argument<string> _outputArgument = new("output")
name: "output", {
description: "The output application package, either .app bundle or ipa$", Description = "The output application package, either .app bundle or ipa$",
parse: ParseAppPackageResult CustomParser = ParseAppPackageResult
); };
private readonly Option<bool> _overwriteOutputOption = new( private readonly Option<bool> _overwriteOutputOption = new("--overwrite")
"--overwrite", {
"Overwrite the output if it already exists" Description = "Overwrite the output if it already exists"
); };
private readonly Option<CompressionLevel> _compressionLevelOption = new( private readonly Option<CompressionLevel> _compressionLevelOption = new("--compression-level")
"--compression-level", {
description: "The compression level for ipa$ archive output", Aliases = { "--level" },
getDefaultValue: () => CompressionLevel.Fastest Description = "The compression level for ipa$ archive output",
); DefaultValueFactory = _ => CompressionLevel.Fastest
};
// //
private readonly Option<IEnumerable<FileInfo>> _itemsOption = new("--items") private readonly Option<IEnumerable<FileInfo>> _itemsOption = new("--items")
{ {
Description = "The entries to inject (Debian packages, Frameworks, and Bundles)", Description = "The entries to inject (Debian packages, Frameworks, and Bundles)",
Aliases = { "-i" },
AllowMultipleArgumentsPerToken = true AllowMultipleArgumentsPerToken = true
}; };
private readonly Option<string> _codesignIdentityOption = new( private readonly Option<FileInfo> _provisioningProfileOption = new("--profile")
"--sign", {
"The identity for code signing (use \"-\" for ad hoc, a.k.a. fake signing)" Aliases = { "-p" },
); Description = "Provisioning profile to extract entitlements, signing identity, and bundle ID"
};
private readonly Option<FileInfo> _codesignEntitlementsOption = new( private readonly Option<bool> _profileBundleIdOption = new("--profile-bundle-id")
"--entitlements", {
"The file containing entitlements that will be written into main executables" Description = "Replace the bundle ID with the one in the provisioning profile"
); };
private readonly Option<string> _codesignIdentityOption = new("--sign")
{
Aliases = { "-s" },
Description = "The identity for code signing (use \"-\" for ad hoc, a.k.a. fake signing)"
};
private readonly Option<FileInfo> _codesignEntitlementsOption = new("--entitlements")
{
Aliases = { "-e" },
Description = "The file containing entitlements that will be written into main executables"
};
// //
private readonly Option<string> _customBundleIdOption = new( private readonly Option<string> _customBundleIdOption = new("--bundle-id")
"--bundleId", {
"The custom identifier that will be applied to application bundles" Aliases = { "-b" },
); Description = "The custom identifier that will be applied to application bundles"
};
private readonly Option<bool> _enableDocumentsSupportOption = new( private readonly Option<bool> _enableDocumentsSupportOption = new("--enable-documents-support")
"--enable-documents-support", {
"Enables documents support (file sharing) for the application" Aliases = { "-d" },
); Description = "Enables documents support (file sharing) for the application"
};
private readonly Option<bool> _removeSupportedDevicesOption = new( private readonly Option<bool> _removeSupportedDevicesOption = new("--remove-supported-devices")
"--remove-supported-devices", {
"Removes supported devices property" Aliases = { "-u" },
); Description = "Removes supported devices property"
};
private readonly Option<IEnumerable<string>> _directoriesToRemoveOption = new("--remove-directories") private readonly Option<IEnumerable<string>> _directoriesToRemoveOption = new("--remove-directories")
{ {
Aliases = { "-r" },
Description = "Directories to remove in the app package, e.g. PlugIns, Watch, AppClip", Description = "Directories to remove in the app package, e.g. PlugIns, Watch, AppClip",
AllowMultipleArgumentsPerToken = true AllowMultipleArgumentsPerToken = true
}; };
internal IviRootCommand() : base("The most demure iOS app injector and signer") internal IviRootCommand() : base("The most demure iOS app injector and signer")
{ {
_itemsOption.AddAlias("-i"); Arguments.Add(_targetArgument);
_codesignIdentityOption.AddAlias("-s"); Arguments.Add(_outputArgument);
_compressionLevelOption.AddAlias("--level"); Options.Add(_overwriteOutputOption);
_codesignEntitlementsOption.AddAlias("-e"); Options.Add(_compressionLevelOption);
_customBundleIdOption.AddAlias("-b"); Options.Add(_itemsOption);
_enableDocumentsSupportOption.AddAlias("-d"); Options.Add(_provisioningProfileOption);
_removeSupportedDevicesOption.AddAlias("-u"); Options.Add(_profileBundleIdOption);
_directoriesToRemoveOption.AddAlias("-r"); Options.Add(_codesignIdentityOption);
Options.Add(_codesignEntitlementsOption);
AddArgument(_targetArgument); Options.Add(_customBundleIdOption);
AddArgument(_outputArgument); Options.Add(_enableDocumentsSupportOption);
AddOption(_overwriteOutputOption); Options.Add(_removeSupportedDevicesOption);
AddOption(_compressionLevelOption); Options.Add(_directoriesToRemoveOption);
AddOption(_itemsOption); SetAction(async parseResult =>
AddOption(_codesignIdentityOption);
AddOption(_codesignEntitlementsOption);
AddOption(_customBundleIdOption);
AddOption(_enableDocumentsSupportOption);
AddOption(_removeSupportedDevicesOption);
AddOption(_directoriesToRemoveOption);
this.SetHandler(async (iviParameters, loggerFactory) =>
{ {
var commandProcessor = new IviRootCommandProcessor(loggerFactory); var binder = new IviRootCommandParametersBinder(
_targetArgument,
_outputArgument,
_overwriteOutputOption,
_compressionLevelOption,
_itemsOption,
_provisioningProfileOption,
_profileBundleIdOption,
_codesignIdentityOption,
_codesignEntitlementsOption,
_customBundleIdOption,
_enableDocumentsSupportOption,
_removeSupportedDevicesOption,
_directoriesToRemoveOption
);
var iviParameters = binder.GetBoundValue(parseResult);
var commandProcessor = new IviRootCommandProcessor();
await commandProcessor.ProcessRootCommand(iviParameters); await commandProcessor.ProcessRootCommand(iviParameters);
}, }
new IviRootCommandParametersBinder(
_targetArgument,
_outputArgument,
_overwriteOutputOption,
_compressionLevelOption,
_itemsOption,
_codesignIdentityOption,
_codesignEntitlementsOption,
_customBundleIdOption,
_enableDocumentsSupportOption,
_removeSupportedDevicesOption,
_directoriesToRemoveOption
),
new LoggerFactoryBinder()
); );
} }
} }

View File

@@ -1,5 +1,4 @@
using System.CommandLine; using System.CommandLine;
using System.CommandLine.Binding;
using System.IO.Compression; using System.IO.Compression;
using ivinject.Features.Command.Models; using ivinject.Features.Command.Models;
using ivinject.Features.Injection.Models; using ivinject.Features.Injection.Models;
@@ -12,49 +11,84 @@ internal class IviRootCommandParametersBinder(
Option<bool> overwriteOutputOption, Option<bool> overwriteOutputOption,
Option<CompressionLevel> compressionLevelOption, Option<CompressionLevel> compressionLevelOption,
Option<IEnumerable<FileInfo>> itemsOption, Option<IEnumerable<FileInfo>> itemsOption,
Option<FileInfo> provisioningProfileOption,
Option<bool> profileBundleIdOption,
Option<string> codesignIdentityOption, Option<string> codesignIdentityOption,
Option<FileInfo> codesignEntitlementsOption, Option<FileInfo> codesignEntitlementsOption,
Option<string> customBundleIdOption, Option<string> customBundleIdOption,
Option<bool> enableDocumentsSupportOption, Option<bool> enableDocumentsSupportOption,
Option<bool> removeSupportedDevicesOption, Option<bool> removeSupportedDevicesOption,
Option<IEnumerable<string>> directoriesToRemoveOption Option<IEnumerable<string>> directoriesToRemoveOption
) : BinderBase<IviParameters> )
{ {
protected override IviParameters GetBoundValue(BindingContext bindingContext) public IviParameters GetBoundValue(ParseResult parseResult)
{ {
var targetAppPackage = var targetAppPackage =
bindingContext.ParseResult.GetValueForArgument(targetArgument); parseResult.GetValue(targetArgument)!;
var outputAppPackage = var outputAppPackage =
bindingContext.ParseResult.GetValueForArgument(outputArgument); parseResult.GetValue(outputArgument)!;
var overwriteOutput = var overwriteOutput =
bindingContext.ParseResult.GetValueForOption(overwriteOutputOption); parseResult.GetValue(overwriteOutputOption);
var compressionLevel = var compressionLevel =
bindingContext.ParseResult.GetValueForOption(compressionLevelOption); parseResult.GetValue(compressionLevelOption);
var items = var items =
bindingContext.ParseResult.GetValueForOption(itemsOption); parseResult.GetValue(itemsOption);
var provisioningProfile =
parseResult.GetValue(provisioningProfileOption);
var profileBundleId =
parseResult.GetValue(profileBundleIdOption);
var codesignIdentity = var codesignIdentity =
bindingContext.ParseResult.GetValueForOption(codesignIdentityOption); parseResult.GetValue(codesignIdentityOption);
var codesignEntitlements = var codesignEntitlements =
bindingContext.ParseResult.GetValueForOption(codesignEntitlementsOption); parseResult.GetValue(codesignEntitlementsOption);
var bundleId = var customBundleId =
bindingContext.ParseResult.GetValueForOption(customBundleIdOption); parseResult.GetValue(customBundleIdOption);
var enableDocumentsSupport = var enableDocumentsSupport =
bindingContext.ParseResult.GetValueForOption(enableDocumentsSupportOption); parseResult.GetValue(enableDocumentsSupportOption);
var removeSupportedDevices = var removeSupportedDevices =
bindingContext.ParseResult.GetValueForOption(removeSupportedDevicesOption); parseResult.GetValue(removeSupportedDevicesOption);
var directoriesToRemove = var directoriesToRemove =
bindingContext.ParseResult.GetValueForOption(directoriesToRemoveOption); parseResult.GetValue(directoriesToRemoveOption);
IviPackagingInfo? packagingInfo; IviSigningInfo? signingInfo = null;
IviPackagingInfo? packagingInfo = null;
if (bundleId is null var profileInfo = provisioningProfile is not null
&& directoriesToRemove is null ? ProvisioningProfileParser.Parse(provisioningProfile)
&& !enableDocumentsSupport : null;
&& !removeSupportedDevices)
packagingInfo = null; if (profileInfo is not null)
else {
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 packagingInfo = new IviPackagingInfo
{ {
CustomBundleId = bundleId, CustomBundleId = bundleId,
@@ -62,6 +96,7 @@ internal class IviRootCommandParametersBinder(
RemoveSupportedDevices = removeSupportedDevices, RemoveSupportedDevices = removeSupportedDevices,
DirectoriesToRemove = directoriesToRemove ?? [] DirectoriesToRemove = directoriesToRemove ?? []
}; };
}
return new IviParameters return new IviParameters
{ {
@@ -70,13 +105,7 @@ internal class IviRootCommandParametersBinder(
OverwriteOutput = overwriteOutput, OverwriteOutput = overwriteOutput,
CompressionLevel = compressionLevel, CompressionLevel = compressionLevel,
InjectionEntries = items?.Select(item => new IviInjectionEntry(item)) ?? [], InjectionEntries = items?.Select(item => new IviInjectionEntry(item)) ?? [],
SigningInfo = codesignIdentity is null SigningInfo = signingInfo,
? null
: new IviSigningInfo
{
Identity = codesignIdentity,
Entitlements = codesignEntitlements
},
PackagingInfo = packagingInfo PackagingInfo = packagingInfo
}; };
} }

View File

@@ -16,8 +16,10 @@ internal class IviRootCommandProcessor
private readonly InjectionManager _injectionManager; private readonly InjectionManager _injectionManager;
private readonly CodesigningManager _codesigningManager; private readonly CodesigningManager _codesigningManager;
internal IviRootCommandProcessor(ILoggerFactory loggerFactory) internal IviRootCommandProcessor()
{ {
var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
_logger = loggerFactory.CreateLogger("Main"); _logger = loggerFactory.CreateLogger("Main");
_packageManager = new PackageManager( _packageManager = new PackageManager(
loggerFactory.CreateLogger("PackageManager") loggerFactory.CreateLogger("PackageManager")
@@ -77,20 +79,24 @@ internal class IviRootCommandProcessor
if (hasIdentity && !isAdHocSigning && !hasEntitlements) if (hasIdentity && !isAdHocSigning && !hasEntitlements)
CriticalError("Entitlements are required for non ad hoc identity signing."); CriticalError("Entitlements are required for non ad hoc identity signing.");
if (isAdHocSigning && !hasEntitlements) if (hasIdentity)
await _codesigningManager.SaveMainBinaryEntitlementsAsync(); {
if (!await _codesigningManager.SaveMainExecutablesEntitlementsAsync())
{
_logger.LogError(
"Unable to save entitlements for one or more binaries. The package is likely unsigned, and all specified entitlements will be applied."
);
}
}
if (!await _codesigningManager.RemoveSignatureAsync(!hasIdentity || hasEntitlements)) if (!await _codesigningManager.RemoveSignatureAsync())
CriticalError("Unable to remove signature from one or more binaries."); CriticalError("Unable to remove signature from one or more binaries.");
await _injectionManager.InsertLoadCommandsAsync(); await _injectionManager.InsertLoadCommandsAsync();
if (hasIdentity) if (hasIdentity)
{ {
if (!await _codesigningManager.SignAsync( if (!await _codesigningManager.SignPackageAsync(signingInfo!))
signingInfo!.Identity,
isAdHocSigning,
signingInfo.Entitlements))
CriticalError("Unable to sign one or more binaries."); CriticalError("Unable to sign one or more binaries.");
} }
} }

View File

@@ -1,17 +0,0 @@
using System.CommandLine.Binding;
using Microsoft.Extensions.Logging;
namespace ivinject.Features.Command;
internal class LoggerFactoryBinder : BinderBase<ILoggerFactory>
{
protected override ILoggerFactory GetBoundValue(BindingContext bindingContext)
=> GetLoggerFactory();
private static ILoggerFactory GetLoggerFactory()
{
var loggerFactory = LoggerFactory.Create(builder =>
builder.AddConsole());
return loggerFactory;
}
}

View File

@@ -2,6 +2,7 @@ namespace ivinject.Features.Command.Models;
internal class IviSigningInfo internal class IviSigningInfo
{ {
internal bool IsFromProvisioningProfile { get; init; }
internal required string Identity { get; init; } internal required string Identity { get; init; }
internal bool IsAdHocSigning => Identity == "-"; internal bool IsAdHocSigning => Identity == "-";
internal FileInfo? Entitlements { get; init; } internal FileInfo? Entitlements { get; init; }

View File

@@ -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; }
}

View File

@@ -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
};
}
}

View File

@@ -19,7 +19,7 @@ internal class DebManager(IviInjectionEntry debEntry) : IDisposable
new ProcessStartInfo new ProcessStartInfo
{ {
FileName = "tar", 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 new ProcessStartInfo
{ {
FileName = "tar", FileName = "tar",
Arguments = $"-xf {dataArchive}", ArgumentList = { "-xf", dataArchive },
WorkingDirectory = _tempPath WorkingDirectory = _tempPath
} }
); );

View File

@@ -13,7 +13,7 @@ internal static class DependencyExtensions
new ProcessStartInfo new ProcessStartInfo
{ {
FileName = "otool", FileName = "otool",
Arguments = $"-L {binary.FullName}", ArgumentList = { "-L", binary.FullName },
RedirectStandardOutput = true RedirectStandardOutput = true
} }
); );
@@ -35,7 +35,7 @@ internal static class DependencyExtensions
new ProcessStartInfo new ProcessStartInfo
{ {
FileName = "install_name_tool", FileName = "install_name_tool",
Arguments = $"-change {oldPath} {newPath} {binary.FullName}", ArgumentList = { "-change", oldPath, newPath, binary.FullName },
RedirectStandardError = true RedirectStandardError = true
} }
); );
@@ -52,7 +52,7 @@ internal static class DependencyExtensions
new ProcessStartInfo new ProcessStartInfo
{ {
FileName = "install_name_tool", FileName = "install_name_tool",
Arguments = $"-add_rpath {rPath} {binary.FullName}", ArgumentList = { "-add_rpath", rPath, binary.FullName },
RedirectStandardOutput = true RedirectStandardOutput = true
} }
); );
@@ -70,7 +70,7 @@ internal static class DependencyExtensions
new ProcessStartInfo new ProcessStartInfo
{ {
FileName = "insert-dylib", FileName = "insert-dylib",
Arguments = $"{dependency} {binary.FullName} --all-yes --inplace", ArgumentList = { dependency, binary.FullName, "--all-yes", "--inplace" },
RedirectStandardOutput = true RedirectStandardOutput = true
} }
); );

View File

@@ -31,6 +31,6 @@ internal static class InfoPlistDictionaryExtensions
internal static string BundleExecutable(this NSDictionary dictionary) => internal static string BundleExecutable(this NSDictionary dictionary) =>
((NSString)dictionary[CoreFoundationBundleExecutableKey]).Content; ((NSString)dictionary[CoreFoundationBundleExecutableKey]).Content;
internal static async Task SaveToFile(this NSDictionary dictionary, string filePath) => internal static async Task SaveToFileAsync(this NSDictionary dictionary, string filePath) =>
await File.WriteAllTextAsync(filePath, dictionary.ToXmlPropertyList()); await File.WriteAllTextAsync(filePath, dictionary.ToXmlPropertyList());
} }

View File

@@ -77,7 +77,7 @@ internal partial class PackageManager
var newBundleId = bundleId.Replace(packageBundleId, customBundleId); var newBundleId = bundleId.Replace(packageBundleId, customBundleId);
dictionary[CoreFoundationBundleIdentifierKey] = new NSString(newBundleId); dictionary[CoreFoundationBundleIdentifierKey] = new NSString(newBundleId);
await dictionary.SaveToFile(file); await dictionary.SaveToFileAsync(file);
replacedCount++; replacedCount++;
} }
@@ -94,7 +94,7 @@ internal partial class PackageManager
if (packagingInfo.EnableDocumentsSupport) if (packagingInfo.EnableDocumentsSupport)
EnableDocumentSupport(); EnableDocumentSupport();
await _infoDictionary.SaveToFile(_infoDictionaryFile.FullName); await _infoDictionary.SaveToFileAsync(_infoDictionaryFile.FullName);
if (packagingInfo.CustomBundleId is not { } customBundleId) if (packagingInfo.CustomBundleId is not { } customBundleId)
return; return;

View File

@@ -3,12 +3,12 @@ using ivinject.Features.Command;
namespace ivinject; namespace ivinject;
internal class Program internal static class Program
{ {
private static readonly RootCommand RootCommand = new IviRootCommand(); private static readonly RootCommand RootCommand = new IviRootCommand();
private static async Task<int> Main(string[] args) private static async Task<int> Main(string[] args)
{ {
return await RootCommand.InvokeAsync(args); return await RootCommand.Parse(args).InvokeAsync();
} }
} }

View File

@@ -5,9 +5,9 @@ An iOS app injector and signer, the most demure in my opinion. The point is, whi
Thus, feature requests and bug reports are not accepted. You probably would like to use [cyan](https://github.com/asdfzxcvbn/pyzule-rw) instead, which remains highly recommended for widespread public use due to its support. In fact, many things are quite similar: both ivinject and cyan can inject tweaks, frameworks, and bundles, fix dependencies, fake sign, etc. However, there are some crucial differences. Thus, feature requests and bug reports are not accepted. You probably would like to use [cyan](https://github.com/asdfzxcvbn/pyzule-rw) instead, which remains highly recommended for widespread public use due to its support. In fact, many things are quite similar: both ivinject and cyan can inject tweaks, frameworks, and bundles, fix dependencies, fake sign, etc. However, there are some crucial differences.
## The Demureness ## 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 an entirely different project, written in C# with .NET 10. 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 wont support anything except for macOS — I couldnt care less about other platforms. - ivinject does not and wont support anything except for macOS — I couldnt care less about other platforms.
@@ -15,6 +15,6 @@ Thus, feature requests and bug reports are not accepted. You probably would like
## Prerequisites ## Prerequisites
* Make sure Xcode is installed * 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` * 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) * 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)

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@@ -17,10 +17,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> <PackageReference Include="System.CommandLine" Version="2.0.0-rc.1.25451.107" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0-rc.2.24473.5" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0-rc.1.25451.107" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0-rc.2.24473.5" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.0-rc.1.25451.107" />
<PackageReference Include="plist-cil" Version="2.2.0" /> <PackageReference Include="plist-cil" Version="2.3.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>