diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60a5a63 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +bin/ +obj/ +.idea +._* +.DS_Store +/packages/ +riderModule.iml +/_ReSharper.Caches/ \ No newline at end of file diff --git a/Common/DirectoryExtensions.cs b/Common/DirectoryExtensions.cs new file mode 100644 index 0000000..a6b5870 --- /dev/null +++ b/Common/DirectoryExtensions.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/Common/FileStreamExtensions.cs b/Common/FileStreamExtensions.cs new file mode 100644 index 0000000..dc7016e --- /dev/null +++ b/Common/FileStreamExtensions.cs @@ -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); + } +} \ No newline at end of file diff --git a/Common/Models/BinaryHeaders.cs b/Common/Models/BinaryHeaders.cs new file mode 100644 index 0000000..891827a --- /dev/null +++ b/Common/Models/BinaryHeaders.cs @@ -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 + ]; +} \ No newline at end of file diff --git a/Common/Models/IviMachOBinary.cs b/Common/Models/IviMachOBinary.cs new file mode 100644 index 0000000..288c2b3 --- /dev/null +++ b/Common/Models/IviMachOBinary.cs @@ -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 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 Thin() + { + using var process = Process.Start( + new ProcessStartInfo + { + FileName = "lipo", + Arguments = $"-thin arm64 {FullName} -output {FullName}" + } + ); + + await process!.WaitForExitAsync(); + return process.ExitCode == 0; + } +} \ No newline at end of file diff --git a/Common/Models/RegularExpressions.cs b/Common/Models/RegularExpressions.cs new file mode 100644 index 0000000..88b4fd6 --- /dev/null +++ b/Common/Models/RegularExpressions.cs @@ -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(); +} \ No newline at end of file diff --git a/Features/Codesigning/CodesigningMachOExtensions.cs b/Features/Codesigning/CodesigningMachOExtensions.cs new file mode 100644 index 0000000..c0a8b6f --- /dev/null +++ b/Features/Codesigning/CodesigningMachOExtensions.cs @@ -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 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 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 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; + } +} \ No newline at end of file diff --git a/Features/Codesigning/CodesigningManager.cs b/Features/Codesigning/CodesigningManager.cs new file mode 100644 index 0000000..9e0ac43 --- /dev/null +++ b/Features/Codesigning/CodesigningManager.cs @@ -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 _allBinaries = []; + private List 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 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 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 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(); +} \ No newline at end of file diff --git a/Features/Codesigning/Models/IviEncryptionInfo.cs b/Features/Codesigning/Models/IviEncryptionInfo.cs new file mode 100644 index 0000000..5c326a3 --- /dev/null +++ b/Features/Codesigning/Models/IviEncryptionInfo.cs @@ -0,0 +1,9 @@ +using ivinject.Common.Models; + +namespace ivinject.Features.Codesigning.Models; + +internal class IviEncryptionInfo +{ + internal bool IsMainBinaryEncrypted { get; init; } + internal IEnumerable EncryptedBinaries { get; init; } = []; +} \ No newline at end of file diff --git a/Features/Command/IviRootCommand.cs b/Features/Command/IviRootCommand.cs new file mode 100644 index 0000000..4f6c966 --- /dev/null +++ b/Features/Command/IviRootCommand.cs @@ -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 _targetArgument = new( + name: "target", + description: "The application package, either .app bundle or ipa$", + parse: ParseAppPackageResult + ); + + private readonly Argument _outputArgument = new( + name: "output", + description: "The output application package, either .app bundle or ipa$", + parse: ParseAppPackageResult + ); + + private readonly Option _overwriteOutputOption = new( + "--overwrite", + "Overwrite the output if it already exists" + ); + + private readonly Option _compressionLevelOption = new( + "--compression-level", + description: "The compression level for ipa$ archive output", + getDefaultValue: () => CompressionLevel.Fastest + ); + + // + + private readonly Option> _itemsOption = new("--items") + { + Description = "The entries to inject (Debian packages, Frameworks, and Bundles)", + AllowMultipleArgumentsPerToken = true + }; + + private readonly Option _codesignIdentityOption = new( + "--sign", + "The identity for code signing (use \"-\" for ad hoc, a.k.a. fake signing)" + ); + + private readonly Option _codesignEntitlementsOption = new( + "--entitlements", + "The file containing entitlements that will be written into main executables" + ); + + // + + private readonly Option _customBundleIdOption = new( + "--bundleId", + "The custom identifier that will be applied to application bundles" + ); + + private readonly Option _enableDocumentsSupportOption = new( + "--enable-documents-support", + "Enables documents support (file sharing) for the application" + ); + + private readonly Option _removeSupportedDevicesOption = new( + "--remove-supported-devices", + "Removes supported devices property" + ); + + private readonly Option> _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() + ); + } +} \ No newline at end of file diff --git a/Features/Command/IviRootCommandParametersBinder.cs b/Features/Command/IviRootCommandParametersBinder.cs new file mode 100644 index 0000000..14bfa81 --- /dev/null +++ b/Features/Command/IviRootCommandParametersBinder.cs @@ -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 targetArgument, + Argument outputArgument, + Option overwriteOutputOption, + Option compressionLevelOption, + Option> itemsOption, + Option codesignIdentityOption, + Option codesignEntitlementsOption, + Option customBundleIdOption, + Option enableDocumentsSupportOption, + Option removeSupportedDevicesOption, + Option> directoriesToRemoveOption +) : BinderBase +{ + 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 + }; + } +} \ No newline at end of file diff --git a/Features/Command/IviRootCommandProcessor.cs b/Features/Command/IviRootCommandProcessor.cs new file mode 100644 index 0000000..544f014 --- /dev/null +++ b/Features/Command/IviRootCommandProcessor.cs @@ -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 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(); + } +} \ No newline at end of file diff --git a/Features/Command/LoggerFactoryBinder.cs b/Features/Command/LoggerFactoryBinder.cs new file mode 100644 index 0000000..37e8bed --- /dev/null +++ b/Features/Command/LoggerFactoryBinder.cs @@ -0,0 +1,17 @@ +using System.CommandLine.Binding; +using Microsoft.Extensions.Logging; + +namespace ivinject.Features.Command; + +internal class LoggerFactoryBinder : BinderBase +{ + protected override ILoggerFactory GetBoundValue(BindingContext bindingContext) + => GetLoggerFactory(); + private static ILoggerFactory GetLoggerFactory() + { + var loggerFactory = LoggerFactory.Create(builder => + builder.AddConsole()); + + return loggerFactory; + } +} \ No newline at end of file diff --git a/Features/Command/Models/IviPackagingInfo.cs b/Features/Command/Models/IviPackagingInfo.cs new file mode 100644 index 0000000..cae2252 --- /dev/null +++ b/Features/Command/Models/IviPackagingInfo.cs @@ -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 DirectoriesToRemove { get; init; } +} \ No newline at end of file diff --git a/Features/Command/Models/IviParameters.cs b/Features/Command/Models/IviParameters.cs new file mode 100644 index 0000000..d2bd5d1 --- /dev/null +++ b/Features/Command/Models/IviParameters.cs @@ -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 InjectionEntries { get; init; } + internal IviSigningInfo? SigningInfo { get; init; } + internal IviPackagingInfo? PackagingInfo { get; init; } +} \ No newline at end of file diff --git a/Features/Command/Models/IviSigningInfo.cs b/Features/Command/Models/IviSigningInfo.cs new file mode 100644 index 0000000..c901f76 --- /dev/null +++ b/Features/Command/Models/IviSigningInfo.cs @@ -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; } +} \ No newline at end of file diff --git a/Features/Injection/DebManager.cs b/Features/Injection/DebManager.cs new file mode 100644 index 0000000..7a80548 --- /dev/null +++ b/Features/Injection/DebManager.cs @@ -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 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); +} \ No newline at end of file diff --git a/Features/Injection/DependencyExtensions.cs b/Features/Injection/DependencyExtensions.cs new file mode 100644 index 0000000..cfe4d08 --- /dev/null +++ b/Features/Injection/DependencyExtensions.cs @@ -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 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 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> AllDependencies(this List copiedBinaries) + { + return (await Task.WhenAll( + copiedBinaries.Select(async binary => await binary.Binary.GetSharedLibraries()) + )) + .SelectMany(dependencies => dependencies) + .Distinct(); + } +} \ No newline at end of file diff --git a/Features/Injection/InjectionManager.cs b/Features/Injection/InjectionManager.cs new file mode 100644 index 0000000..aa81374 --- /dev/null +++ b/Features/Injection/InjectionManager.cs @@ -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 _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 PrepareEntriesAsync( + IviInjectionEntry[] entries, + List 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 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 files) + { + var debManagers = new List(); + + var entries = await PrepareEntriesAsync(files.ToArray(), debManagers); + CopyEntries(entries); + + debManagers.ForEach(manager => manager.Dispose()); + } + + internal async Task 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); + } + } +} \ No newline at end of file diff --git a/Features/Injection/Models/IviCopiedBinary.cs b/Features/Injection/Models/IviCopiedBinary.cs new file mode 100644 index 0000000..c2b881c --- /dev/null +++ b/Features/Injection/Models/IviCopiedBinary.cs @@ -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(); + } +} \ No newline at end of file diff --git a/Features/Injection/Models/IviInjectionEntry.cs b/Features/Injection/Models/IviInjectionEntry.cs new file mode 100644 index 0000000..6f4df1f --- /dev/null +++ b/Features/Injection/Models/IviInjectionEntry.cs @@ -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) + }; +} diff --git a/Features/Injection/Models/IviInjectionEntryType.cs b/Features/Injection/Models/IviInjectionEntryType.cs new file mode 100644 index 0000000..a3d0f1d --- /dev/null +++ b/Features/Injection/Models/IviInjectionEntryType.cs @@ -0,0 +1,11 @@ +namespace ivinject.Features.Injection.Models; + +internal enum IviInjectionEntryType +{ + DynamicLibrary, + DebianPackage, + Bundle, + Framework, + PlugIn, + Unknown +} \ No newline at end of file diff --git a/Features/Injection/Models/RegularExpressions.cs b/Features/Injection/Models/RegularExpressions.cs new file mode 100644 index 0000000..b38bd0d --- /dev/null +++ b/Features/Injection/Models/RegularExpressions.cs @@ -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(); +} \ No newline at end of file diff --git a/Features/Packaging/InfoPlistDictionaryExtensions.cs b/Features/Packaging/InfoPlistDictionaryExtensions.cs new file mode 100644 index 0000000..1ae0d15 --- /dev/null +++ b/Features/Packaging/InfoPlistDictionaryExtensions.cs @@ -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()); +} \ No newline at end of file diff --git a/Features/Packaging/Models/DirectoryNames.cs b/Features/Packaging/Models/DirectoryNames.cs new file mode 100644 index 0000000..f395153 --- /dev/null +++ b/Features/Packaging/Models/DirectoryNames.cs @@ -0,0 +1,7 @@ +namespace ivinject.Features.Packaging.Models; + +internal class DirectoryNames +{ + internal const string FrameworksDirectoryName = "Frameworks"; + internal const string PlugInsDirectoryName = "PlugIns"; +} \ No newline at end of file diff --git a/Features/Packaging/Models/InfoPlistDictionaryKeys.cs b/Features/Packaging/Models/InfoPlistDictionaryKeys.cs new file mode 100644 index 0000000..870202a --- /dev/null +++ b/Features/Packaging/Models/InfoPlistDictionaryKeys.cs @@ -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"; +} \ No newline at end of file diff --git a/Features/Packaging/Models/IviDirectoriesInfo.cs b/Features/Packaging/Models/IviDirectoriesInfo.cs new file mode 100644 index 0000000..d5d1855 --- /dev/null +++ b/Features/Packaging/Models/IviDirectoriesInfo.cs @@ -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); +} \ No newline at end of file diff --git a/Features/Packaging/Models/IviPackageInfo.cs b/Features/Packaging/Models/IviPackageInfo.cs new file mode 100644 index 0000000..5362089 --- /dev/null +++ b/Features/Packaging/Models/IviPackageInfo.cs @@ -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; +} \ No newline at end of file diff --git a/Features/Packaging/PackageManager.cs b/Features/Packaging/PackageManager.cs new file mode 100644 index 0000000..c3a8c54 --- /dev/null +++ b/Features/Packaging/PackageManager.cs @@ -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); +} \ No newline at end of file diff --git a/Features/Packaging/PackageManager_Modifications.cs b/Features/Packaging/PackageManager_Modifications.cs new file mode 100644 index 0000000..00045ae --- /dev/null +++ b/Features/Packaging/PackageManager_Modifications.cs @@ -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 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); + } +} \ No newline at end of file diff --git a/Features/Packaging/PackageManager_Output.cs b/Features/Packaging/PackageManager_Output.cs new file mode 100644 index 0000000..b0e5b8d --- /dev/null +++ b/Features/Packaging/PackageManager_Output.cs @@ -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); + } +} \ No newline at end of file diff --git a/Images/Banner.png b/Images/Banner.png new file mode 100644 index 0000000..31bc244 Binary files /dev/null and b/Images/Banner.png differ diff --git a/KnownFrameworks/CydiaSubstrate.framework/CydiaSubstrate b/KnownFrameworks/CydiaSubstrate.framework/CydiaSubstrate new file mode 100644 index 0000000..d6f5960 Binary files /dev/null and b/KnownFrameworks/CydiaSubstrate.framework/CydiaSubstrate differ diff --git a/KnownFrameworks/CydiaSubstrate.framework/Info.plist b/KnownFrameworks/CydiaSubstrate.framework/Info.plist new file mode 100644 index 0000000..6bc23c5 --- /dev/null +++ b/KnownFrameworks/CydiaSubstrate.framework/Info.plist @@ -0,0 +1,25 @@ + + + + + CFBundleExecutable + CydiaSubstrate + + CFBundleIdentifier + ellekit + + CFBundleName + ElleKit + + CFBundleShortVersionString + 1.1.3 + + CFBundleVersion + 1.1.3 + + UIRequiredDeviceCapabilities + + arm64 + + + diff --git a/KnownFrameworks/Orion.framework/Info.plist b/KnownFrameworks/Orion.framework/Info.plist new file mode 100644 index 0000000..a681923 Binary files /dev/null and b/KnownFrameworks/Orion.framework/Info.plist differ diff --git a/KnownFrameworks/Orion.framework/Orion b/KnownFrameworks/Orion.framework/Orion new file mode 100755 index 0000000..5f91eb3 Binary files /dev/null and b/KnownFrameworks/Orion.framework/Orion differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e95a14e --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..6369a62 --- /dev/null +++ b/Program.cs @@ -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 Main(string[] args) + { + return await RootCommand.InvokeAsync(args); + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e447b8 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ + + +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 won’t support anything except for macOS — I couldn’t 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) \ No newline at end of file diff --git a/ivinject.csproj b/ivinject.csproj new file mode 100644 index 0000000..6496e52 --- /dev/null +++ b/ivinject.csproj @@ -0,0 +1,26 @@ + + + + Exe + net9.0 + enable + enable + + true + + true + Speed + true + + true + true + + + + + + + + + + diff --git a/ivinject.sln b/ivinject.sln new file mode 100644 index 0000000..a6289a0 --- /dev/null +++ b/ivinject.sln @@ -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