First commit

This commit is contained in:
eevee
2024-11-03 18:29:07 +03:00
commit db63ef6ff2
41 changed files with 1742 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
bin/
obj/
.idea
._*
.DS_Store
/packages/
riderModule.iml
/_ReSharper.Caches/

View File

@@ -0,0 +1,41 @@
namespace ivinject.Common;
internal static class DirectoryExtensions
{
internal static string TempDirectoryPath()
{
return Path.Combine(
Path.GetTempPath(),
Path.ChangeExtension(Path.GetRandomFileName(), null)
);
}
internal static string HomeDirectoryPath() =>
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
internal static void CopyDirectory(string sourceDir, string destinationDir, bool recursive)
{
var dir = new DirectoryInfo(sourceDir);
if (!dir.Exists)
throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}");
var dirs = dir.GetDirectories();
Directory.CreateDirectory(destinationDir);
foreach (var file in dir.GetFiles())
{
var targetFilePath = Path.Combine(destinationDir, file.Name);
file.CopyTo(targetFilePath);
}
if (!recursive) return;
foreach (var subDir in dirs)
{
var newDestinationDir = Path.Combine(destinationDir, subDir.Name);
CopyDirectory(subDir.FullName, newDestinationDir, true);
}
}
}

View File

@@ -0,0 +1,14 @@
namespace ivinject.Common;
internal static class FileStreamExtensions
{
internal static uint FileHeader(this FileStream stream)
{
using var reader = new BinaryReader(stream);
var bytes = reader.ReadBytes(4);
return bytes.Length != 4
? uint.MinValue
: BitConverter.ToUInt32(bytes);
}
}

View File

@@ -0,0 +1,30 @@
namespace ivinject.Common.Models;
internal class BinaryHeaders
{
private const uint FatMagic = 0xcafebabe;
private const uint FatMagic64 = 0xcafebabf;
private const uint FatCigam = 0xbebafeca;
private const uint FatCigam64 = 0xbfbafeca;
private const uint MhMagic = 0xfeedface;
private const uint MhMagic64 = 0xfeedfacf;
private const uint MhCigam = 0xcefaedfe;
private const uint MhCigam64 = 0xcffaedfe;
internal static readonly uint[] FatHeaders =
[
FatMagic,
FatMagic64,
FatCigam,
FatCigam64
];
internal static readonly uint[] MhHeaders =
[
MhMagic,
MhMagic64,
MhCigam,
MhCigam64
];
}

View File

@@ -0,0 +1,68 @@
using System.Diagnostics;
using static ivinject.Common.Models.BinaryHeaders;
namespace ivinject.Common.Models;
internal class IviMachOBinary(string fileName)
{
private FileInfo FileInfo { get; } = new(fileName);
internal string Name => FileInfo.Name;
internal string FullName => FileInfo.FullName;
internal bool IsFatFile
{
get
{
var header = File.OpenRead(FullName).FileHeader();
return FatHeaders.Contains(header);
}
}
internal string FileSize
{
get
{
FileInfo.Refresh();
var size = FileInfo.Length;
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",
_ => $"{(size >> 30) / (float)1024:F1} GB"
};
}
}
internal async Task<bool> IsEncrypted()
{
using var process = Process.Start(
new ProcessStartInfo
{
FileName = "otool",
Arguments = $"-l {FullName}",
RedirectStandardOutput = true
}
);
var output = await process!.StandardOutput.ReadToEndAsync();
return RegularExpressions.OToolEncryptedBinary().IsMatch(output);
}
internal async Task<bool> Thin()
{
using var process = Process.Start(
new ProcessStartInfo
{
FileName = "lipo",
Arguments = $"-thin arm64 {FullName} -output {FullName}"
}
);
await process!.WaitForExitAsync();
return process.ExitCode == 0;
}
}

View File

@@ -0,0 +1,12 @@
using System.Text.RegularExpressions;
namespace ivinject.Common.Models;
internal static partial class RegularExpressions
{
[GeneratedRegex("cryptid 1", RegexOptions.Compiled)]
internal static partial Regex OToolEncryptedBinary();
[GeneratedRegex(@"\.(?:app|\w*ipa)$", RegexOptions.Compiled)]
internal static partial Regex ApplicationPackage();
}

View File

@@ -0,0 +1,70 @@
using System.Diagnostics;
using System.Text;
using ivinject.Common.Models;
namespace ivinject.Features.Codesigning;
internal static class CodesigningMachOExtensions
{
internal static async Task<bool> SignAsync(
this IviMachOBinary binary,
string identity,
bool force,
FileInfo? entitlements,
bool preserveEntitlements = false
)
{
var arguments = new StringBuilder($"-s {identity}");
if (entitlements is not null)
arguments.Append($" --entitlements {entitlements.FullName}");
else if (preserveEntitlements)
arguments.Append(" --preserve-metadata=entitlements");
if (force)
arguments.Append(" -f");
arguments.Append($" {binary.FullName}");
using var process = Process.Start(
new ProcessStartInfo
{
FileName = "codesign",
Arguments = arguments.ToString(),
RedirectStandardOutput = true
}
);
await process!.WaitForExitAsync();
return process.ExitCode == 0;
}
internal static async Task<bool> RemoveSignatureAsync(this IviMachOBinary binary)
{
using var process = Process.Start(
new ProcessStartInfo
{
FileName = "codesign",
Arguments = $"--remove-signature {binary.FullName}"
}
);
await process!.WaitForExitAsync();
return process.ExitCode == 0;
}
internal static async Task<bool> DumpEntitlementsAsync(this IviMachOBinary binary, string outputFilePath)
{
using var process = Process.Start(
new ProcessStartInfo
{
FileName = "codesign",
Arguments = $"-d --entitlements {outputFilePath} --xml {binary.FullName}",
RedirectStandardError = true
}
);
await process!.WaitForExitAsync();
return process.ExitCode == 0;
}
}

View File

@@ -0,0 +1,159 @@
using ivinject.Common;
using ivinject.Common.Models;
using ivinject.Features.Codesigning.Models;
using ivinject.Features.Packaging.Models;
using Microsoft.Extensions.Logging;
using static ivinject.Features.Packaging.Models.DirectoryNames;
using static ivinject.Common.Models.BinaryHeaders;
namespace ivinject.Features.Codesigning;
internal class CodesigningManager(ILogger logger) : IDisposable
{
private IviPackageInfo _packageInfo = null!;
private readonly List<IviMachOBinary> _allBinaries = [];
private List<IviMachOBinary> Binaries => _allBinaries[1..];
private IviMachOBinary MainBinary => _allBinaries[0];
private FileInfo? _savedMainEntitlements;
internal void UpdateWithPackage(IviPackageInfo packageInfo)
{
_packageInfo = packageInfo;
ProcessBinaries(packageInfo);
}
private void ProcessBinaries(IviPackageInfo packageInfo)
{
foreach (var file in Directory.EnumerateFiles(
packageInfo.DirectoriesInfo.BundleDirectory,
"*",
SearchOption.AllDirectories))
{
var header = File.OpenRead(file).FileHeader();
if (!MhHeaders.Contains(header) && !FatHeaders.Contains(header))
continue;
if (file.Contains("Stub"))
{
var relativePath = Path.GetRelativePath(
packageInfo.DirectoriesInfo.BundleDirectory,
file
);
logger.LogWarning(
"Skipping stub executable {}, its signature may not be modified",
relativePath
);
continue;
}
_allBinaries.Add(new IviMachOBinary(file));
}
}
internal async Task<IviEncryptionInfo> GetEncryptionStateAsync()
{
var results = await Task.WhenAll(
Binaries.Select(
async binary => new
{
Binary = binary,
IsEncrypted = await binary.IsEncrypted()
}
));
return new IviEncryptionInfo
{
IsMainBinaryEncrypted = await MainBinary.IsEncrypted(),
EncryptedBinaries = results
.Where(result => result.IsEncrypted)
.Select(result => result.Binary)
};
}
internal async Task SaveMainBinaryEntitlementsAsync()
{
var tempFile = Path.GetTempFileName();
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(
_packageInfo.DirectoriesInfo.BundleDirectory,
binary.FullName
).Contains(FrameworksDirectoryName);
if (isMainExecutable)
mainExecutablesCount++;
return await binary.SignAsync(
identity,
isAdHocSigning,
isMainExecutable
? entitlements
: null,
isMainExecutable && entitlements is null
);
}
)
);
if (signingResults.Any(result => !result))
return false;
if (!await MainBinary.SignAsync(identity, false, _savedMainEntitlements ?? entitlements))
return false;
logger.LogInformation(
"Signed {} binaries ({} main executables) with the specified identity",
_allBinaries.Count,
mainExecutablesCount + 1 // App main executable
);
return true;
}
internal async Task<bool> RemoveSignatureAsync(bool allBinaries)
{
if (allBinaries)
{
var removingResults = await Task.WhenAll(
_allBinaries.Select(binary => binary.RemoveSignatureAsync())
);
if (removingResults.Any(result => !result))
return false;
logger.LogInformation("Signature removed from {} binaries", _allBinaries.Count);
return true;
}
if (!await MainBinary.RemoveSignatureAsync())
return false;
logger.LogInformation("Removed {} signature", MainBinary.Name);
return true;
}
public void Dispose() => _savedMainEntitlements?.Delete();
}

View File

@@ -0,0 +1,9 @@
using ivinject.Common.Models;
namespace ivinject.Features.Codesigning.Models;
internal class IviEncryptionInfo
{
internal bool IsMainBinaryEncrypted { get; init; }
internal IEnumerable<IviMachOBinary> EncryptedBinaries { get; init; } = [];
}

View File

@@ -0,0 +1,133 @@
using System.CommandLine;
using System.CommandLine.Parsing;
using System.IO.Compression;
using ivinject.Common.Models;
namespace ivinject.Features.Command;
internal class IviRootCommand : RootCommand
{
private static string ParseAppPackageResult(ArgumentResult result)
{
var value = result.Tokens[0].Value;
if (!RegularExpressions.ApplicationPackage().IsMatch(value))
result.ErrorMessage = "The application package must be either an .app bundle or an .ipa$ archive.";
return value;
}
//
private readonly Argument<string> _targetArgument = new(
name: "target",
description: "The application package, either .app bundle or ipa$",
parse: ParseAppPackageResult
);
private readonly Argument<string> _outputArgument = new(
name: "output",
description: "The output application package, either .app bundle or ipa$",
parse: ParseAppPackageResult
);
private readonly Option<bool> _overwriteOutputOption = new(
"--overwrite",
"Overwrite the output if it already exists"
);
private readonly Option<CompressionLevel> _compressionLevelOption = new(
"--compression-level",
description: "The compression level for ipa$ archive output",
getDefaultValue: () => CompressionLevel.Fastest
);
//
private readonly Option<IEnumerable<FileInfo>> _itemsOption = new("--items")
{
Description = "The entries to inject (Debian packages, Frameworks, and Bundles)",
AllowMultipleArgumentsPerToken = true
};
private readonly Option<string> _codesignIdentityOption = new(
"--sign",
"The identity for code signing (use \"-\" for ad hoc, a.k.a. fake signing)"
);
private readonly Option<FileInfo> _codesignEntitlementsOption = new(
"--entitlements",
"The file containing entitlements that will be written into main executables"
);
//
private readonly Option<string> _customBundleIdOption = new(
"--bundleId",
"The custom identifier that will be applied to application bundles"
);
private readonly Option<bool> _enableDocumentsSupportOption = new(
"--enable-documents-support",
"Enables documents support (file sharing) for the application"
);
private readonly Option<bool> _removeSupportedDevicesOption = new(
"--remove-supported-devices",
"Removes supported devices property"
);
private readonly Option<IEnumerable<string>> _directoriesToRemoveOption = new("--remove-directories")
{
Description = "Directories to remove in the app package, e.g. PlugIns, Watch, AppClip",
AllowMultipleArgumentsPerToken = true
};
internal IviRootCommand() : base("The most demure iOS app injector and signer")
{
_itemsOption.AddAlias("-i");
_codesignIdentityOption.AddAlias("-s");
_compressionLevelOption.AddAlias("--level");
_codesignEntitlementsOption.AddAlias("-e");
_customBundleIdOption.AddAlias("-b");
_enableDocumentsSupportOption.AddAlias("-d");
_removeSupportedDevicesOption.AddAlias("-u");
_directoriesToRemoveOption.AddAlias("-r");
AddArgument(_targetArgument);
AddArgument(_outputArgument);
AddOption(_overwriteOutputOption);
AddOption(_compressionLevelOption);
AddOption(_itemsOption);
AddOption(_codesignIdentityOption);
AddOption(_codesignEntitlementsOption);
AddOption(_customBundleIdOption);
AddOption(_enableDocumentsSupportOption);
AddOption(_removeSupportedDevicesOption);
AddOption(_directoriesToRemoveOption);
this.SetHandler(async (iviParameters, loggerFactory) =>
{
var commandProcessor = new IviRootCommandProcessor(loggerFactory);
await commandProcessor.ProcessRootCommand(iviParameters);
},
new IviRootCommandParametersBinder(
_targetArgument,
_outputArgument,
_overwriteOutputOption,
_compressionLevelOption,
_itemsOption,
_codesignIdentityOption,
_codesignEntitlementsOption,
_customBundleIdOption,
_enableDocumentsSupportOption,
_removeSupportedDevicesOption,
_directoriesToRemoveOption
),
new LoggerFactoryBinder()
);
}
}

View File

@@ -0,0 +1,83 @@
using System.CommandLine;
using System.CommandLine.Binding;
using System.IO.Compression;
using ivinject.Features.Command.Models;
using ivinject.Features.Injection.Models;
namespace ivinject.Features.Command;
internal class IviRootCommandParametersBinder(
Argument<string> targetArgument,
Argument<string> outputArgument,
Option<bool> overwriteOutputOption,
Option<CompressionLevel> compressionLevelOption,
Option<IEnumerable<FileInfo>> itemsOption,
Option<string> codesignIdentityOption,
Option<FileInfo> codesignEntitlementsOption,
Option<string> customBundleIdOption,
Option<bool> enableDocumentsSupportOption,
Option<bool> removeSupportedDevicesOption,
Option<IEnumerable<string>> directoriesToRemoveOption
) : BinderBase<IviParameters>
{
protected override IviParameters GetBoundValue(BindingContext bindingContext)
{
var targetAppPackage =
bindingContext.ParseResult.GetValueForArgument(targetArgument);
var outputAppPackage =
bindingContext.ParseResult.GetValueForArgument(outputArgument);
var overwriteOutput =
bindingContext.ParseResult.GetValueForOption(overwriteOutputOption);
var compressionLevel =
bindingContext.ParseResult.GetValueForOption(compressionLevelOption);
var items =
bindingContext.ParseResult.GetValueForOption(itemsOption);
var codesignIdentity =
bindingContext.ParseResult.GetValueForOption(codesignIdentityOption);
var codesignEntitlements =
bindingContext.ParseResult.GetValueForOption(codesignEntitlementsOption);
var bundleId =
bindingContext.ParseResult.GetValueForOption(customBundleIdOption);
var enableDocumentsSupport =
bindingContext.ParseResult.GetValueForOption(enableDocumentsSupportOption);
var removeSupportedDevices =
bindingContext.ParseResult.GetValueForOption(removeSupportedDevicesOption);
var directoriesToRemove =
bindingContext.ParseResult.GetValueForOption(directoriesToRemoveOption);
IviPackagingInfo? packagingInfo;
if (bundleId is null
&& directoriesToRemove is null
&& !enableDocumentsSupport
&& !removeSupportedDevices)
packagingInfo = null;
else
packagingInfo = new IviPackagingInfo
{
CustomBundleId = bundleId,
EnableDocumentsSupport = enableDocumentsSupport,
RemoveSupportedDevices = removeSupportedDevices,
DirectoriesToRemove = directoriesToRemove ?? []
};
return new IviParameters
{
TargetAppPackage = targetAppPackage,
OutputAppPackage = outputAppPackage,
OverwriteOutput = overwriteOutput,
CompressionLevel = compressionLevel,
InjectionEntries = items?.Select(item => new IviInjectionEntry(item)) ?? [],
SigningInfo = codesignIdentity is null
? null
: new IviSigningInfo
{
Identity = codesignIdentity,
Entitlements = codesignEntitlements
},
PackagingInfo = packagingInfo
};
}
}

View File

@@ -0,0 +1,130 @@
using System.Diagnostics.CodeAnalysis;
using ivinject.Features.Codesigning;
using ivinject.Features.Command.Models;
using ivinject.Features.Injection;
using ivinject.Features.Injection.Models;
using ivinject.Features.Packaging;
using ivinject.Features.Packaging.Models;
using Microsoft.Extensions.Logging;
namespace ivinject.Features.Command;
internal class IviRootCommandProcessor
{
private readonly ILogger _logger;
private readonly PackageManager _packageManager;
private readonly InjectionManager _injectionManager;
private readonly CodesigningManager _codesigningManager;
internal IviRootCommandProcessor(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger("Main");
_packageManager = new PackageManager(
loggerFactory.CreateLogger("PackageManager")
);
_injectionManager = new InjectionManager(
loggerFactory.CreateLogger("InjectionManager")
);
_codesigningManager = new CodesigningManager(
loggerFactory.CreateLogger("CodesigningManager")
);
}
[SuppressMessage("Usage", "CA2254")]
private void CriticalError(string? message, params object?[] args)
{
_logger.LogCritical(message, args);
Environment.Exit(1);
}
private async Task InjectEntries(IEnumerable<IviInjectionEntry> injectionEntries)
{
await _injectionManager.AddEntriesAsync(injectionEntries);
if (!await _injectionManager.ThinCopiedBinariesAsync())
CriticalError("Unable to thin one or more binaries.");
await _injectionManager.CopyKnownFrameworksAsync();
await _injectionManager.FixCopiedDependenciesAsync();
}
private async Task CheckForEncryptedBinaries(IviPackageInfo packageInfo)
{
var encryptionInfo = await _codesigningManager.GetEncryptionStateAsync();
if (encryptionInfo.IsMainBinaryEncrypted)
CriticalError("The main application binary, {}, is encrypted.", packageInfo.MainBinary.Name);
if (encryptionInfo.EncryptedBinaries.Any())
{
var encryptedPaths = encryptionInfo.EncryptedBinaries.Select(binary =>
Path.GetRelativePath(packageInfo.DirectoriesInfo.BundleDirectory, binary.FullName)
);
_logger.LogError(
"The app package contains encrypted binaries. Consider removing them: \n{}",
string.Join("\n", encryptedPaths)
);
}
}
private async Task ProcessSigning(IviSigningInfo? signingInfo)
{
var hasIdentity = signingInfo is not null;
var isAdHocSigning = signingInfo?.IsAdHocSigning ?? false;
var hasEntitlements = signingInfo?.Entitlements is not null;
if (hasIdentity && !isAdHocSigning && !hasEntitlements)
CriticalError("Entitlements are required for non ad hoc identity signing.");
if (isAdHocSigning && !hasEntitlements)
await _codesigningManager.SaveMainBinaryEntitlementsAsync();
if (!await _codesigningManager.RemoveSignatureAsync(!hasIdentity || hasEntitlements))
CriticalError("Unable to remove signature from one or more binaries.");
await _injectionManager.InsertLoadCommandsAsync();
if (hasIdentity)
{
if (!await _codesigningManager.SignAsync(
signingInfo!.Identity,
isAdHocSigning,
signingInfo.Entitlements))
CriticalError("Unable to sign one or more binaries.");
}
}
internal async Task ProcessRootCommand(IviParameters parameters)
{
_packageManager.LoadAppPackage(parameters.TargetAppPackage);
_logger.LogInformation("Loaded app package");
var packageInfo = _packageManager.PackageInfo;
if (parameters.PackagingInfo is { } packagingInfo)
await _packageManager.PerformPackageModifications(packagingInfo);
//
_injectionManager.UpdateWithPackage(packageInfo);
await InjectEntries(parameters.InjectionEntries);
_codesigningManager.UpdateWithPackage(packageInfo);
await CheckForEncryptedBinaries(packageInfo);
await ProcessSigning(parameters.SigningInfo);
if (!_packageManager.CreateAppPackage(
parameters.OutputAppPackage,
parameters.OverwriteOutput,
parameters.CompressionLevel
))
CriticalError(
"The app package couldn't be created. If it already exists, use --overwrite to replace."
);
_codesigningManager.Dispose();
_packageManager.Dispose();
}
}

View File

@@ -0,0 +1,17 @@
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

@@ -0,0 +1,9 @@
namespace ivinject.Features.Command.Models;
internal class IviPackagingInfo
{
internal string? CustomBundleId { get; init; }
internal bool RemoveSupportedDevices { get; init; }
internal bool EnableDocumentsSupport { get; init; }
internal required IEnumerable<string> DirectoriesToRemove { get; init; }
}

View File

@@ -0,0 +1,15 @@
using System.IO.Compression;
using ivinject.Features.Injection.Models;
namespace ivinject.Features.Command.Models;
internal class IviParameters
{
internal required string TargetAppPackage { get; init; }
internal required string OutputAppPackage { get; init; }
internal bool OverwriteOutput { get; init; }
internal CompressionLevel CompressionLevel { get; init; }
internal required IEnumerable<IviInjectionEntry> InjectionEntries { get; init; }
internal IviSigningInfo? SigningInfo { get; init; }
internal IviPackagingInfo? PackagingInfo { get; init; }
}

View File

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

View File

@@ -0,0 +1,60 @@
using System.Diagnostics;
using ivinject.Features.Injection.Models;
using static ivinject.Common.DirectoryExtensions;
namespace ivinject.Features.Injection;
internal class DebManager(IviInjectionEntry debEntry) : IDisposable
{
private readonly string _tempPath = TempDirectoryPath();
internal async Task<IviInjectionEntry[]> ExtractDebEntries()
{
if (debEntry.Type is not IviInjectionEntryType.DebianPackage)
throw new ArgumentException("Entry type is not DebianPackage");
Directory.CreateDirectory(_tempPath);
var process = Process.Start(
new ProcessStartInfo
{
FileName = "tar",
Arguments = $"-xf {debEntry.FullName} --directory={_tempPath}"
}
);
await process!.WaitForExitAsync();
var dataArchive = Directory.GetFiles(_tempPath, "data*.*")[0];
process = Process.Start(
new ProcessStartInfo
{
FileName = "tar",
Arguments = $"-xf {dataArchive}",
WorkingDirectory = _tempPath
}
);
await process!.WaitForExitAsync();
var dataFiles = Directory.EnumerateFiles(
_tempPath,
"*",
SearchOption.AllDirectories
);
var dataDirectories = Directory.EnumerateDirectories(
_tempPath,
"*",
SearchOption.AllDirectories
);
return dataFiles.Concat(dataDirectories)
.Select(entry => new IviInjectionEntry(entry))
.Where(entry => entry.Type is not IviInjectionEntryType.Unknown)
.ToArray();
}
public void Dispose() => Directory.Delete(_tempPath, true);
}

View File

@@ -0,0 +1,89 @@
using System.Diagnostics;
using ivinject.Common.Models;
using ivinject.Features.Injection.Models;
using RegularExpressions = ivinject.Features.Injection.Models.RegularExpressions;
namespace ivinject.Features.Injection;
internal static class DependencyExtensions
{
internal static async Task<string[]> GetSharedLibraries(this IviMachOBinary binary)
{
using var process = Process.Start(
new ProcessStartInfo
{
FileName = "otool",
Arguments = $"-L {binary.FullName}",
RedirectStandardOutput = true
}
);
var output = await process!.StandardOutput.ReadToEndAsync();
var matches = RegularExpressions.OToolSharedLibrary().Matches(output);
// the first result is actually LC_ID_DYLIB, not LC_LOAD_DYLIB
return matches.Select(match => match.Groups[1].Value).ToArray()[1..];
}
internal static async Task ChangeDependency(
this IviMachOBinary binary,
string oldPath,
string newPath
)
{
using var process = Process.Start(
new ProcessStartInfo
{
FileName = "install_name_tool",
Arguments = $"-change {oldPath} {newPath} {binary.FullName}",
RedirectStandardError = true
}
);
await process!.WaitForExitAsync();
}
internal static async Task<bool> AddRunPath(
this IviMachOBinary binary,
string rPath
)
{
using var process = Process.Start(
new ProcessStartInfo
{
FileName = "install_name_tool",
Arguments = $"-add_rpath {rPath} {binary.FullName}",
RedirectStandardOutput = true
}
);
await process!.WaitForExitAsync();
return process.ExitCode == 0;
}
internal static async Task InsertDependency(
this IviMachOBinary binary,
string dependency
)
{
using var process = Process.Start(
new ProcessStartInfo
{
FileName = "insert-dylib",
Arguments = $"{dependency} {binary.FullName} --all-yes --inplace",
RedirectStandardOutput = true
}
);
await process!.WaitForExitAsync();
}
internal static async Task<IEnumerable<string>> AllDependencies(this List<IviCopiedBinary> copiedBinaries)
{
return (await Task.WhenAll(
copiedBinaries.Select(async binary => await binary.Binary.GetSharedLibraries())
))
.SelectMany(dependencies => dependencies)
.Distinct();
}
}

View File

@@ -0,0 +1,231 @@
using Claunia.PropertyList;
using ivinject.Common.Models;
using ivinject.Features.Injection.Models;
using ivinject.Features.Packaging;
using ivinject.Features.Packaging.Models;
using Microsoft.Extensions.Logging;
using static ivinject.Common.DirectoryExtensions;
namespace ivinject.Features.Injection;
internal class InjectionManager(ILogger logger)
{
private readonly List<IviCopiedBinary> _copiedBinaries = [];
private IviPackageInfo _packageInfo = null!;
private static readonly IviInjectionEntry[] KnownFrameworkEntries = Directory.GetDirectories(
Path.Combine(HomeDirectoryPath(), ".ivinject"),
"*.framework"
)
.Select(framework => new IviInjectionEntry(framework))
.ToArray();
private static readonly IviInjectionEntry OrionFramework = KnownFrameworkEntries
.Single(framework => framework.Name == "Orion.framework");
private static readonly IviInjectionEntry SubstrateFramework = KnownFrameworkEntries
.Single(framework => framework.Name == "CydiaSubstrate.framework");
internal void UpdateWithPackage(IviPackageInfo packageInfo) =>
_packageInfo = packageInfo;
private async Task<IviInjectionEntry[]> PrepareEntriesAsync(
IviInjectionEntry[] entries,
List<IDisposable> debManagers
)
{
var finalEntries = entries.Where(entry =>
entry.Type is not IviInjectionEntryType.DebianPackage
).ToList();
var debFiles = entries.Where(entry =>
entry.Type is IviInjectionEntryType.DebianPackage
);
foreach (var debFile in debFiles)
{
var name = debFile.Name;
var debManager = new DebManager(debFile);
var debEntries = await debManager.ExtractDebEntries();
logger.LogInformation("{} entries within {} will be injected", debEntries.Length, name);
finalEntries.AddRange(debEntries);
debManagers.Add(debManager);
}
return finalEntries.ToArray();
}
private void CopyEntries(IEnumerable<IviInjectionEntry> entries)
{
foreach (var entry in entries)
{
var isReplaced = false;
var pathInBundle = entry.GetPathInBundle(_packageInfo.DirectoriesInfo);
var isDynamicLibrary = entry.Type is IviInjectionEntryType.DynamicLibrary;
if (isDynamicLibrary || entry.Type is IviInjectionEntryType.Unknown)
{
if (File.Exists(pathInBundle))
{
File.Delete(pathInBundle);
isReplaced = true;
}
File.Copy(entry.FullName, pathInBundle);
if (isDynamicLibrary)
_copiedBinaries.Add(
new IviCopiedBinary
{
Binary = new IviMachOBinary(pathInBundle),
Type = entry.Type
}
);
}
else
{
if (Directory.Exists(pathInBundle))
{
Directory.Delete(pathInBundle, true);
isReplaced = true;
}
CopyDirectory(entry.FullName, pathInBundle, true);
if (entry.Type is not IviInjectionEntryType.Bundle) {
var infoDictionaryPath = Path.Combine(pathInBundle, "Info.plist");
var infoDictionary = (NSDictionary)PropertyListParser.Parse(infoDictionaryPath);
_copiedBinaries.Add(
new IviCopiedBinary
{
Binary = new IviMachOBinary(
Path.Combine(pathInBundle, infoDictionary.BundleExecutable())
),
Type = entry.Type
}
);
}
}
logger.LogInformation("{} {}", isReplaced ? "Replaced" : "Copied", entry.Name);
}
}
internal async Task AddEntriesAsync(IEnumerable<IviInjectionEntry> files)
{
var debManagers = new List<IDisposable>();
var entries = await PrepareEntriesAsync(files.ToArray(), debManagers);
CopyEntries(entries);
debManagers.ForEach(manager => manager.Dispose());
}
internal async Task<bool> ThinCopiedBinariesAsync()
{
var fatBinaries =
_copiedBinaries.Select(binary => binary.Binary).Where(binary => binary.IsFatFile);
foreach (var fatBinary in fatBinaries)
{
var previousSize = fatBinary.FileSize;
if (!await fatBinary.Thin())
return false;
logger.LogInformation(
"Thinned {} ({} -> {})",
fatBinary.Name,
previousSize,
fatBinary.FileSize
);
}
return true;
}
internal async Task CopyKnownFrameworksAsync()
{
var allDependencies = await _copiedBinaries.AllDependencies();
var entries = KnownFrameworkEntries.Where(framework =>
allDependencies.Any(dependency => dependency.Contains(framework.Name))
).ToList();
if (entries.Contains(OrionFramework) && !entries.Contains(SubstrateFramework))
entries.Add(SubstrateFramework);
CopyEntries(entries);
}
internal async Task FixCopiedDependenciesAsync()
{
var copiedNames = _copiedBinaries.Select(binary => binary.Name);
foreach (var binary in _copiedBinaries.Select(binary => binary.Binary))
{
var dependencies = await binary.GetSharedLibraries();
var brokenDependencies = dependencies.Where(dependency =>
!dependency.StartsWith('@') && copiedNames.Any(dependency.Contains)
);
foreach (var dependency in brokenDependencies)
{
var copiedBinary = _copiedBinaries.Single(copiedBinary =>
dependency.Contains(copiedBinary.Name)
);
var newPath = copiedBinary.GetRunPath(_packageInfo.DirectoriesInfo);
await binary.ChangeDependency(dependency, newPath);
logger.LogInformation(
"Fixed dependency path in {} ({} -> {})",
binary.Name,
dependency,
newPath
);
}
}
}
internal async Task InsertLoadCommandsAsync()
{
var mainBinary = _packageInfo.MainBinary;
var copiedDependencies = (await _copiedBinaries.AllDependencies())
.Where(dependency => dependency.StartsWith('@'));
var mainBinaryDependencies = await mainBinary.GetSharedLibraries();
// if (mainBinaryDependencies.All(dependency => !dependency.StartsWith("@rpath")))
// {
// await mainBinary.AddRunPath("@executable_path/Frameworks");
// logger.LogInformation("Added Frameworks to {}'s run path", mainBinary.Name);
// }
var dependenciesToInsert = _copiedBinaries
.Where(binary =>
binary.Type is IviInjectionEntryType.DynamicLibrary or IviInjectionEntryType.Framework
)
.Where(binary =>
copiedDependencies.All(dependency => !dependency.Contains(binary.Name))
);
foreach (var dependency in dependenciesToInsert)
{
if (mainBinaryDependencies.Contains(dependency.Name))
continue;
var runPath = dependency.GetRunPath(_packageInfo.DirectoriesInfo);
await mainBinary.InsertDependency(runPath);
logger.LogInformation("Inserted load command {} into {}", runPath, mainBinary.Name);
}
}
}

View File

@@ -0,0 +1,35 @@
using System.Text;
using ivinject.Common.Models;
using ivinject.Features.Packaging.Models;
namespace ivinject.Features.Injection.Models;
internal class IviCopiedBinary
{
internal required IviInjectionEntryType Type { get; init; }
internal required IviMachOBinary Binary { get; init; }
internal string Name => Binary.Name;
internal string GetRunPath(IviDirectoriesInfo directoriesInfo)
{
var builder = new StringBuilder("@rpath/");
builder.Append(
Type switch
{
IviInjectionEntryType.Framework => Path.GetRelativePath(
directoriesInfo.FrameworksDirectory,
Binary.FullName
),
IviInjectionEntryType.PlugIn => Path.GetRelativePath(
directoriesInfo.PlugInsDirectory,
Binary.FullName
),
_ => Binary.Name
}
);
return builder.ToString();
}
}

View File

@@ -0,0 +1,46 @@
using ivinject.Features.Packaging.Models;
namespace ivinject.Features.Injection.Models;
internal class IviInjectionEntry
{
private readonly FileInfo _fileInfo;
internal string FullName => _fileInfo.FullName;
internal string Name => _fileInfo.Name;
internal IviInjectionEntryType Type => _fileInfo.Extension switch
{
".dylib" => IviInjectionEntryType.DynamicLibrary,
".deb" => IviInjectionEntryType.DebianPackage,
".bundle" => IviInjectionEntryType.Bundle,
".framework" => IviInjectionEntryType.Framework,
".appex" => IviInjectionEntryType.PlugIn,
_ => IviInjectionEntryType.Unknown
};
internal IviInjectionEntry(FileInfo fileInfo)
=> _fileInfo = fileInfo;
internal IviInjectionEntry(string filePath)
=> _fileInfo = new FileInfo(filePath);
internal string GetPathInBundle(IviDirectoriesInfo directoriesInfo) =>
Type switch
{
IviInjectionEntryType.DynamicLibrary or IviInjectionEntryType.Unknown =>
Path.Combine(
Type is IviInjectionEntryType.DynamicLibrary
? directoriesInfo.FrameworksDirectory
: directoriesInfo.BundleDirectory,
Name
),
IviInjectionEntryType.Framework => Path.Combine(
directoriesInfo.FrameworksDirectory,
Name
),
IviInjectionEntryType.PlugIn => Path.Combine(
directoriesInfo.PlugInsDirectory,
Name
),
_ => Path.Combine(directoriesInfo.BundleDirectory, Name)
};
}

View File

@@ -0,0 +1,11 @@
namespace ivinject.Features.Injection.Models;
internal enum IviInjectionEntryType
{
DynamicLibrary,
DebianPackage,
Bundle,
Framework,
PlugIn,
Unknown
}

View File

@@ -0,0 +1,9 @@
using System.Text.RegularExpressions;
namespace ivinject.Features.Injection.Models;
internal static partial class RegularExpressions
{
[GeneratedRegex(@"([\/@].*) \(.*\)", RegexOptions.Compiled)]
internal static partial Regex OToolSharedLibrary();
}

View File

@@ -0,0 +1,36 @@
using Claunia.PropertyList;
using static ivinject.Features.Packaging.Models.InfoPlistDictionaryKeys;
namespace ivinject.Features.Packaging;
internal static class InfoPlistDictionaryExtensions
{
internal static string? BundleIdentifier(this NSDictionary dictionary) =>
dictionary.TryGetValue(CoreFoundationBundleIdentifierKey, out var bundleIdObject)
? ((NSString)bundleIdObject).Content
: null;
internal static string? WatchKitCompanionAppBundleIdentifier(this NSDictionary dictionary) =>
dictionary.TryGetValue(WatchKitCompanionAppBundleIdentifierKey, out var bundleIdObject)
? ((NSString)bundleIdObject).Content
: null;
internal static NSDictionary? Extension(this NSDictionary dictionary) =>
dictionary.TryGetValue(NextStepExtensionKey, out var extension)
? (NSDictionary)extension
: null;
internal static string ExtensionPointIdentifier(this NSDictionary dictionary) =>
((NSString)dictionary[NextStepExtensionPointIdentifierKey]).Content;
internal static NSDictionary ExtensionAttributes(this NSDictionary dictionary) =>
(NSDictionary)dictionary[NextStepExtensionAttributesKey];
internal static string WatchKitAppBundleIdentifier(this NSDictionary dictionary) =>
((NSString)dictionary[WatchKitAppBundleIdentifierKey]).Content;
internal static string BundleExecutable(this NSDictionary dictionary) =>
((NSString)dictionary[CoreFoundationBundleExecutableKey]).Content;
internal static async Task SaveToFile(this NSDictionary dictionary, string filePath) =>
await File.WriteAllTextAsync(filePath, dictionary.ToXmlPropertyList());
}

View File

@@ -0,0 +1,7 @@
namespace ivinject.Features.Packaging.Models;
internal class DirectoryNames
{
internal const string FrameworksDirectoryName = "Frameworks";
internal const string PlugInsDirectoryName = "PlugIns";
}

View File

@@ -0,0 +1,16 @@
namespace ivinject.Features.Packaging.Models;
internal static class InfoPlistDictionaryKeys
{
internal const string UiKitSupportedDevicesKey = "UISupportedDevices";
internal const string UiKitSupportsDocumentBrowserKey = "UISupportsDocumentBrowser";
internal const string UiKitFileSharingEnabledKey = "UIFileSharingEnabled";
internal const string CoreFoundationBundleIdentifierKey = "CFBundleIdentifier";
internal const string CoreFoundationBundleExecutableKey = "CFBundleExecutable";
internal const string WatchKitCompanionAppBundleIdentifierKey = "WKCompanionAppBundleIdentifier";
internal const string NextStepExtensionKey = "NSExtension";
internal const string NextStepExtensionPointIdentifierKey = "NSExtensionPointIdentifier";
internal const string NextStepExtensionAttributesKey = "NSExtensionAttributes";
internal const string WatchKitAppBundleIdentifierKey = "WKAppBundleIdentifier";
}

View File

@@ -0,0 +1,10 @@
using static ivinject.Features.Packaging.Models.DirectoryNames;
namespace ivinject.Features.Packaging.Models;
internal class IviDirectoriesInfo(string bundleDirectory)
{
internal string BundleDirectory { get; } = bundleDirectory;
internal string FrameworksDirectory { get; } = Path.Combine(bundleDirectory, FrameworksDirectoryName);
internal string PlugInsDirectory { get; } = Path.Combine(bundleDirectory, PlugInsDirectoryName);
}

View File

@@ -0,0 +1,12 @@
using ivinject.Common.Models;
namespace ivinject.Features.Packaging.Models;
internal class IviPackageInfo(string mainBinary, string bundleIdentifier, IviDirectoriesInfo directoriesInfo)
{
internal IviMachOBinary MainBinary { get; } = new(
Path.Combine(directoriesInfo.BundleDirectory, mainBinary)
);
internal string BundleIdentifier { get; } = bundleIdentifier;
internal IviDirectoriesInfo DirectoriesInfo { get; } = directoriesInfo;
}

View File

@@ -0,0 +1,67 @@
using System.IO.Compression;
using Claunia.PropertyList;
using ivinject.Features.Packaging.Models;
using Microsoft.Extensions.Logging;
using static ivinject.Common.DirectoryExtensions;
namespace ivinject.Features.Packaging;
internal partial class PackageManager(ILogger logger) : IDisposable
{
private readonly string _tempDirectory = TempDirectoryPath();
private string TempPayloadDirectory => Path.Combine(_tempDirectory, "Payload");
private string _bundleDirectory = null!;
private FileInfo _infoDictionaryFile = null!;
private NSDictionary _infoDictionary = null!;
internal IviPackageInfo PackageInfo { get; private set; } = null!;
private void LoadPackageInfo()
{
_infoDictionaryFile = new FileInfo(
Path.Combine(_bundleDirectory, "Info.plist")
);
_infoDictionary = (NSDictionary)PropertyListParser.Parse(_infoDictionaryFile);
PackageInfo = new IviPackageInfo(
_infoDictionary.BundleExecutable(),
((NSString)_infoDictionary["CFBundleIdentifier"]).Content,
new IviDirectoriesInfo(_bundleDirectory)
);
}
private void ProcessAppPackage(string targetAppPackage)
{
var directoryInfo = new DirectoryInfo(targetAppPackage);
if (directoryInfo.Exists)
{
var packageName = directoryInfo.Name;
_bundleDirectory = Path.Combine(TempPayloadDirectory, packageName);
CopyDirectory(targetAppPackage, _bundleDirectory, true);
logger.LogInformation("Copied {}", packageName);
return;
}
var fileInfo = new FileInfo(targetAppPackage);
var fileName = fileInfo.Name;
ZipFile.ExtractToDirectory(targetAppPackage, _tempDirectory);
logger.LogInformation("Extracted {}", fileName);
_bundleDirectory = Directory.GetDirectories(TempPayloadDirectory)[0];
}
internal void LoadAppPackage(string targetAppPackage)
{
ProcessAppPackage(targetAppPackage);
LoadPackageInfo();
}
public void Dispose() => Directory.Delete(_tempDirectory, true);
}

View File

@@ -0,0 +1,104 @@
using Claunia.PropertyList;
using ivinject.Features.Command.Models;
using Microsoft.Extensions.Logging;
using static ivinject.Features.Packaging.Models.InfoPlistDictionaryKeys;
namespace ivinject.Features.Packaging;
internal partial class PackageManager
{
private void RemoveSupportedDevices()
{
if (_infoDictionary.Remove(UiKitSupportedDevicesKey))
logger.LogInformation("Removed supported devices property");
else
logger.LogWarning("Unable to remove supported devices property. The key is likely not present.");
}
private void EnableDocumentSupport()
{
_infoDictionary[UiKitSupportsDocumentBrowserKey] = new NSNumber(true);
_infoDictionary[UiKitFileSharingEnabledKey] = new NSNumber(true);
logger.LogInformation("Enabled documents support for the application");
}
private void RemoveDirectories(IEnumerable<string> directories)
{
foreach (var directory in directories)
{
Directory.Delete(Path.Combine(_bundleDirectory, directory), true);
logger.LogInformation("Removed {} directory from the app package", directory);
}
}
private static void ReplaceWatchKitIdentifiers(
NSDictionary dictionary,
string customBundleId,
string packageBundleId
)
{
if (dictionary.WatchKitCompanionAppBundleIdentifier() is not null)
dictionary[WatchKitCompanionAppBundleIdentifierKey] = new NSString(customBundleId);
if (dictionary.Extension() is not { } extension
|| extension.ExtensionPointIdentifier() != "com.apple.watchkit")
return;
var attributes = extension.ExtensionAttributes();
var watchKitAppBundleId = attributes.WatchKitAppBundleIdentifier();
attributes[WatchKitAppBundleIdentifierKey] = new NSString(
watchKitAppBundleId.Replace(packageBundleId, customBundleId)
);
}
private async Task ReplaceBundleIdentifiers(string customBundleId)
{
var packageBundleId = PackageInfo.BundleIdentifier;
var replacedCount = 0;
var infoPlistFiles = Directory.EnumerateFiles(
_bundleDirectory,
"Info.plist",
SearchOption.AllDirectories
);
foreach (var file in infoPlistFiles)
{
var dictionary = (NSDictionary)PropertyListParser.Parse(file);
ReplaceWatchKitIdentifiers(dictionary, customBundleId, packageBundleId);
if (dictionary.BundleIdentifier() is not { } bundleId
|| !bundleId.Contains(packageBundleId))
continue;
var newBundleId = bundleId.Replace(packageBundleId, customBundleId);
dictionary[CoreFoundationBundleIdentifierKey] = new NSString(newBundleId);
await dictionary.SaveToFile(file);
replacedCount++;
}
logger.LogInformation("Replaced bundle identifier of {} bundles", replacedCount);
}
internal async Task PerformPackageModifications(IviPackagingInfo packagingInfo)
{
RemoveDirectories(packagingInfo.DirectoriesToRemove);
if (packagingInfo.RemoveSupportedDevices)
RemoveSupportedDevices();
if (packagingInfo.EnableDocumentsSupport)
EnableDocumentSupport();
await _infoDictionary.SaveToFile(_infoDictionaryFile.FullName);
if (packagingInfo.CustomBundleId is not { } customBundleId)
return;
await ReplaceBundleIdentifiers(customBundleId);
}
}

View File

@@ -0,0 +1,81 @@
using System.IO.Compression;
using Microsoft.Extensions.Logging;
using static ivinject.Common.DirectoryExtensions;
namespace ivinject.Features.Packaging;
internal partial class PackageManager
{
private bool CopyAppPackage(string outputAppPackage, bool overwrite, ref bool isOverwritten)
{
var packageDirectory = new DirectoryInfo(outputAppPackage);
if (packageDirectory.Exists)
{
if (overwrite)
{
packageDirectory.Delete(true);
isOverwritten = true;
}
else
{
return false;
}
}
CopyDirectory(_bundleDirectory, packageDirectory.FullName, true);
logger.LogInformation("{} {}", isOverwritten ? "Replaced" : "Copied", packageDirectory.Name);
return true;
}
private bool CreateAppArchive(
string outputAppPackage,
bool overwrite,
CompressionLevel compressionLevel,
ref bool isOverwritten
)
{
var packageFile = new FileInfo(outputAppPackage);
if (packageFile.Exists)
{
if (overwrite)
{
packageFile.Delete();
isOverwritten = true;
}
else
{
return false;
}
}
foreach (var dotFile in Directory.EnumerateFiles(TempPayloadDirectory, ".*"))
{
var fileInfo = new FileInfo(dotFile);
File.Delete(fileInfo.FullName);
logger.LogWarning("Removed {} from the app package", fileInfo.Name);
}
ZipFile.CreateFromDirectory(
TempPayloadDirectory,
packageFile.FullName,
compressionLevel,
true
);
logger.LogInformation("{} {}", isOverwritten ? "Replaced" : "Created", packageFile.Name);
return true;
}
internal bool CreateAppPackage(string outputAppPackage, bool overwrite, CompressionLevel compressionLevel)
{
var isOverwritten = false;
return outputAppPackage.EndsWith(".app")
? CopyAppPackage(outputAppPackage, overwrite, ref isOverwritten)
: CreateAppArchive(outputAppPackage, overwrite, compressionLevel, ref isOverwritten);
}
}

BIN
Images/Banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>CydiaSubstrate</string>
<key>CFBundleIdentifier</key>
<string>ellekit</string>
<key>CFBundleName</key>
<string>ElleKit</string>
<key>CFBundleShortVersionString</key>
<string>1.1.3</string>
<key>CFBundleVersion</key>
<string>1.1.3</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
</dict>
</plist>

Binary file not shown.

Binary file not shown.

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 whoeevee
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

14
Program.cs Normal file
View File

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

20
README.md Normal file
View File

@@ -0,0 +1,20 @@
<img src="Images/Banner.png" width="256"/>
An iOS app injector and signer, the most demure in my opinion. The point is, while developing, I was primarily accommodating myself and made the injector exactly as I wanted to see it. Well, I'm happy to use it. The EeveeSpotify official IPAs are also created with ivinject starting from version 5.8.
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
- **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 does not and wont support anything except for macOS — I couldnt care less about other platforms.
- Some more differences like ivinject supports more bundle types for signing and package modifications, such as Extensions or Watch; forcefully thins binaries; does not and won't support configuration files, etc.
## Prerequisites
* Make sure Xcode is installed
* Install insert-dylib (`brew install --HEAD 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)

26
ivinject.csproj Normal file
View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<Optimize>true</Optimize>
<OptimizationPreference>Speed</OptimizationPreference>
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
<StripSymbols>true</StripSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0-rc.2.24473.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0-rc.2.24473.5" />
<PackageReference Include="plist-cil" Version="2.2.0" />
</ItemGroup>
</Project>

16
ivinject.sln Normal file
View File

@@ -0,0 +1,16 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ivinject", "ivinject.csproj", "{F5D4F0AB-BD7F-46CF-B6E0-46A9B6D5E7C3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F5D4F0AB-BD7F-46CF-B6E0-46A9B6D5E7C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5D4F0AB-BD7F-46CF-B6E0-46A9B6D5E7C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5D4F0AB-BD7F-46CF-B6E0-46A9B6D5E7C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5D4F0AB-BD7F-46CF-B6E0-46A9B6D5E7C3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal