mirror of
https://github.com/whoeevee/ivinject.git
synced 2026-01-09 00:25:03 +01:00
First commit
This commit is contained in:
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user