diff --git a/cyan/__main__.py b/cyan/__main__.py index 9cd2456..383ab72 100755 --- a/cyan/__main__.py +++ b/cyan/__main__.py @@ -43,6 +43,10 @@ def main() -> None: "-s", "--fakesign", action="store_true", help="fakesign all binaries for use with appsync/trollstore" ) + parser.add_argument( + "-q", "--thin", action="store_true", + help="thin all binaries to arm64, may largely reduce size" + ) parser.add_argument( "-c", "--compress", metavar="level", type=int, default=6, @@ -53,6 +57,10 @@ def main() -> None: "--ignore-encrypted", action="store_true", help="skip main binary encryption check" ) + parser.add_argument( + "--overwrite", action="store_true", + help="overwrite existing files without confirming" + ) parser.add_argument( "-v", "--version", action="version", version="cyan v1.0b" diff --git a/cyan/logic.py b/cyan/logic.py index 782c893..f5c84eb 100644 --- a/cyan/logic.py +++ b/cyan/logic.py @@ -49,6 +49,8 @@ def main(parser: ArgumentParser) -> None: app.plist.enable_documents() if args.fakesign: app.fakesign_all() + if args.thin: + app.thin_all() # create subdirectories if necessary if "/" in args.o: @@ -56,7 +58,7 @@ def main(parser: ArgumentParser) -> None: # done ! if OUTPUT_IS_IPA: - print("[*] generating ipa..") + print(f"[*] generating ipa with compression level {args.compress}..") tbhutils.make_ipa(tmpdir, args.o, args.compress) print(f"[*] generated ipa at {args.o}") else: diff --git a/cyan/tbhtypes/app_bundle.py b/cyan/tbhtypes/app_bundle.py index 332859d..5b3604d 100644 --- a/cyan/tbhtypes/app_bundle.py +++ b/cyan/tbhtypes/app_bundle.py @@ -1,6 +1,7 @@ import os import shutil from glob import glob +from typing import Optional from .executable import Executable from .plist import Plist @@ -15,6 +16,8 @@ class AppBundle: path ) + self.cached_executables: Optional[list[str]] = None + def remove(self, name: str) -> bool: path = f"{self.path}/{name}" if not os.path.exists(path): @@ -38,20 +41,38 @@ class AppBundle: else: print("[?] watch app not present") - def fakesign_all(self) -> None: - self.executable.fakesign() - count = 1 - - for ts in sum(( + def get_executables(self) -> list[str]: + return sum(( + glob(f"{self.path}/**/*.dylib", recursive=True), glob(f"{self.path}/**/*.appex", recursive=True), glob(f"{self.path}/**/*.framework", recursive=True) - ), []): # type: ignore - pl = Plist(f"{ts}/Info.plist") - Executable(f"{ts}/{pl['CFBundleExecutable']}").fakesign() - count += 1 + ), []) # type: ignore - for ts in glob(f"{self.path}/**/*.dylib", recursive=True): - Executable(ts).fakesign() - count += 1 + def mass_operate(self, op: str, func: str) -> None: + # this works since we call this after injecting + if self.cached_executables is None: + self.cached_executables = self.get_executables() + + if getattr(self.executable, func)(): + count = 1 + else: + count = 0 + + for ts in self.cached_executables: + if ts.endswith(".dylib"): + call = getattr(Executable(ts), func)() + else: + pl = Plist(f"{ts}/Info.plist") + call = getattr(Executable(f"{ts}/{pl['CFBundleExecutable']}"), func)() + + if call: + count += 1 + + print(f"[*] {op} \033[96m{count}\033[0m item(s)") + + def fakesign_all(self) -> None: + self.mass_operate("fakesigned", "fakesign") + + def thin_all(self) -> None: + self.mass_operate("thinned", "thin") - print(f"[*] fakesigned \033[96m{count}\033[0m items") diff --git a/cyan/tbhtypes/executable.py b/cyan/tbhtypes/executable.py index 6c1ed1a..6f1960f 100644 --- a/cyan/tbhtypes/executable.py +++ b/cyan/tbhtypes/executable.py @@ -16,6 +16,7 @@ class Executable: install_dir, specific = tbhutils.get_tools_dir() nt = f"{specific}/install_name_tool" ldid = f"{specific}/ldid" + lipo = f"{specific}/lipo" otool = f"{specific}/otool" starters = ("\t/Library/", "\t@rpath", "\t@executable_path") @@ -183,12 +184,22 @@ class Executable: subprocess.run(["ldid", f"-S{ENT_PATH}", self.path]) print("[*] restored entitlements") - def fakesign(self, keep_entitlements: bool = True) -> None: + def fakesign(self, keep_entitlements: bool = True) -> bool: cmd = [self.ldid, "-S"] if keep_entitlements: cmd.append("-M") subprocess.run(cmd + [self.path]) + return True + + def thin(self) -> bool: + if subprocess.run( + [self.lipo, "-thin", "arm64", self.path, "-output", self.path], + stderr=subprocess.DEVNULL + ).returncode == 0: + return True + + return False def change_dependency(self, old: str, new: str) -> None: subprocess.run([self.nt, "-change", old, new, self.path]) diff --git a/cyan/tbhtypes/plist.py b/cyan/tbhtypes/plist.py index 38deae1..f5ed185 100644 --- a/cyan/tbhtypes/plist.py +++ b/cyan/tbhtypes/plist.py @@ -54,3 +54,4 @@ class Plist: print("[*] enabled documents support") else: print("[?] documents support was already enabled") + diff --git a/cyan/tbhutils.py b/cyan/tbhutils.py index 402df2e..2d42e9d 100644 --- a/cyan/tbhutils.py +++ b/cyan/tbhutils.py @@ -22,18 +22,21 @@ def validate_inputs(args: Namespace) -> Optional[str]: return f"{args.i} does not exist" if os.path.exists(args.o): - try: - overwrite = input( - f"[<] {args.o} already exists, overwrite it? [Y/n] " - if args.output is not None - else "[<] no output was specified. overwrite the input? [Y/n] " - ).strip().lower() - except KeyboardInterrupt: - sys.exit("[>] bye!") + if args.overwrite: + print(f"[*] {args.o} already exists; overwriting") + else: + try: + overwrite = input( + f"[<] {args.o} already exists, overwrite it? [Y/n] " + if args.output is not None + else "[<] no output was specified. overwrite the input? [Y/n] " + ).strip().lower() + except KeyboardInterrupt: + sys.exit("[>] bye!") - if overwrite not in ("y", "yes", ""): - print("[>] quitting.") - sys.exit(0) + if overwrite not in ("y", "yes", ""): + print("[>] quitting.") + sys.exit(0) if args.f is not None: # dictionary ensures unique names