52 Commits
v1.0 ... v1.4.3

Author SHA1 Message Date
zx
f7b0169ee9 chore: bump version to v1.4.3 2025-02-01 21:39:41 -05:00
zx
ffbff5c78d feat: use multiple cyans 2025-02-01 21:38:15 -05:00
zx
eba32c22e6 chore: bump version to v1.4.2 (stable) 2025-01-08 19:10:32 -05:00
zx
a96632f1b0 chore: ignore DS_Store
macos moment
2025-01-08 19:06:52 -05:00
zx
86e45cc186 feat(cgen): plist and entitlement merging 2024-11-26 22:21:13 -05:00
zx
e09351287b feat: plist merging 2024-11-26 22:07:22 -05:00
zx
403e5d65a5 chore: bump version to v1.4.1 2024-11-07 22:32:58 -05:00
zx
354a1d9ac5 fix: not auto-injecting already fixed dependency names
whoops
2024-11-07 22:31:43 -05:00
zx
73d1495eff chore: bump version to v1.4 (stable) 2024-11-02 10:43:45 -04:00
zx
31169d6c65 refactor: move orion check out of loop 2024-11-02 10:42:47 -04:00
zx
0808fd8c1b feat: manually modified dylib support (#6) 2024-10-31 14:35:22 -04:00
zx
8c873a84c8 chore: bump version to v1.3 2024-10-28 17:31:19 -04:00
zx
7543bb3d8d fix: can't extract entitlements with codesign (ProcursusTeam/ldid#45)
this issue was originally reported in #8
2024-10-28 17:27:02 -04:00
zx
c0b0324a12 chore: move injection functions to MainExecutable 2024-10-25 23:04:40 -04:00
zx
42517c90fb chore: move write_entitlements() to MainExecutable
the func is only used in `MainExecutable`, so we should move it there to make it a bit more "clean"
2024-10-25 22:44:25 -04:00
zx
b4e3e6aaf5 chore: add question issue template 2024-10-15 12:14:53 -04:00
zx
b60788d167 chore: bump version to v1.2.3 2024-10-14 21:09:10 -04:00
zx
5f37ab3f23 chore: add support for .tipa 2024-10-14 21:08:34 -04:00
zx
4e05892e1d chore: bump version to v1.2.2 2024-10-12 14:56:32 -04:00
zx
54359e0935 fix: don't zip hidden files (lol @whoeevee) 2024-10-12 14:53:42 -04:00
zx
46752d0a26 chore: update orion to v1.0.2 2024-09-30 20:16:19 -04:00
zx
ce20baae4a chore: bump version to v1.2.1
fuck my life
2024-09-18 22:03:48 -04:00
zx
91494bc3a3 turns out ldid can already do this... 2024-09-18 22:02:51 -04:00
zx
edd34f6713 chore: bump version to v1.2 2024-09-18 21:45:21 -04:00
zx
c1c0b2294e feat, refactor: entitlement merging, MainExecutable 2024-09-18 21:39:02 -04:00
zx
2d16015c78 chore: bump version to v1.1.6 2024-09-16 20:26:37 -04:00
zx
4c979a91e7 template: grammar?
idrk
2024-09-16 20:25:55 -04:00
zx
b4aa6c9b69 chore: use enhancement label 2024-09-16 20:24:13 -04:00
zx
246f6ace80 docs: recommended flags
also issue templates
2024-09-16 20:23:19 -04:00
zx
511cb4bbe3 chore: use unzip and zip when available 2024-09-16 20:07:23 -04:00
zx
d1c3bc6908 chore: bump version to v1.1.5 2024-09-15 15:17:40 -04:00
zx
8fe0fc2984 fix: plist changing operations (#3)
lol, this is so fucking stupid, i was trying to fix #3 but also found that `Plist.change()` was being used incorrectly EVERYWHERE in cyan, LMAO

slight oversight since i forgot i had even changed its usage, but at least it's fixed now
2024-09-15 15:16:43 -04:00
zx
e315454544 docs: acknowledgements 2024-09-14 21:01:04 -04:00
zx
9072c91965 docs(install): remove ensurepath step 2024-09-14 20:51:06 -04:00
zx
06a2ea8dad chore: bump version to v1.1.4 2024-09-13 18:28:35 -04:00
zx
55b2ec9d07 fix: folders ending in / 2024-09-13 18:27:45 -04:00
zx
52b3b11a1a chore: use insert_dylib on more platforms 2024-09-13 18:07:00 -04:00
zx
4375b7f880 docs: update install instructions 2024-09-11 17:33:40 -04:00
zx
8f7db1438a chore: bump version to v1.1.3 2024-09-11 07:15:47 -04:00
zx
5e08afc0ac fix(cgen): folders ending in / 2024-09-11 07:14:50 -04:00
zx
0bd6393e78 chore: bundle cgen 2024-09-10 17:35:42 -04:00
zx
f5a5d7b5ed fix: sub-bundle detection 2024-09-10 17:23:30 -04:00
zx
c3a51e7e03 docs: update install instructions 2024-09-10 16:30:51 -04:00
zx
806db413e2 fix: crashing when jailbroken
i realized azule actually removes the signature (instead of fakesigning) before specific operations, maybe this is the fix?
2024-09-10 07:11:15 -04:00
zx
927df65b55 Merge branch 'main' of github.com:asdfzxcvbn/pyzule-rw 2024-09-09 19:37:24 -04:00
zx
851eedf629 fix: using wrong ldid 2024-09-09 19:36:38 -04:00
zx
126d17924b docs: feature requests 2024-09-09 10:32:34 -04:00
zx
5ebab86a44 whoopsies 2024-09-08 22:32:43 -04:00
zx
c8197a8c47 feat: .cyan files 2024-09-08 22:31:26 -04:00
zx
1e9607d9e1 feat: change app icons 2024-09-08 19:29:59 -04:00
zx
30fa189197 fix: validate minimum version 2024-09-08 18:54:35 -04:00
zx
5c0363b5a9 feat: plist operations 2024-09-08 18:48:05 -04:00
23 changed files with 848 additions and 231 deletions

22
.github/ISSUE_TEMPLATE/bug.yaml vendored Normal file
View File

@@ -0,0 +1,22 @@
# thanks to uYouEnhanced for the issue template:
# https://github.com/arichornlover/uYouEnhanced/blob/main/.github/ISSUE_TEMPLATE/bug.yaml?plain=1
name: bug
description: report a bug in cyan
title: "[bug] "
labels: bug
body:
- type: checkboxes
attributes:
label: have you searched the existing issues?
options:
- label: this is a unique issue. i agree that if this isn't a unique issue, i'll be BLOCKED from the cyan repo
required: true
- type: textarea
attributes:
label: describe the bug.
description: attach videos or screenshots if possible
validations:
required: true

View File

@@ -0,0 +1,22 @@
# thanks to uYouEnhanced for the issue template:
# https://github.com/arichornlover/uYouEnhanced/blob/main/.github/ISSUE_TEMPLATE/bug.yaml?plain=1
name: feature request
description: request a feature to be added to cyan
title: "[feature request] "
labels: enhancement
body:
- type: checkboxes
attributes:
label: have you searched the existing issues?
options:
- label: this is a unique feature request. i agree that if this isn't a unique request, i'll be BLOCKED from the cyan repo
required: true
- type: textarea
attributes:
label: describe the feature request.
description: what exactly are you requesting? can you provide an example where this would be useful?
validations:
required: true

22
.github/ISSUE_TEMPLATE/question.yaml vendored Normal file
View File

@@ -0,0 +1,22 @@
# thanks to uYouEnhanced for the issue template:
# https://github.com/arichornlover/uYouEnhanced/blob/main/.github/ISSUE_TEMPLATE/bug.yaml?plain=1
name: question
description: have a question about cyan or its code? use this template!
title: "[question] "
labels: question
body:
- type: checkboxes
attributes:
label: have you searched the existing issues?
options:
- label: this is a unique question. i agree that if this isn't a unique question, i'll be BLOCKED from the cyan repo
required: true
- type: textarea
attributes:
label: what's up?
description: what's the question?
validations:
required: true

5
.gitignore vendored
View File

@@ -1,5 +1,8 @@
__pycache__/
build/
*.egg-info
*.egg-info/
*.cyan
.DS_Store

View File

@@ -1,49 +1,64 @@
# pyzule-rw / cyan
rewriting [pyzule](https://github.com/asdfzxcvbn/pyzule), but actually good this time! btw.. cyan/pyzule looks best with a monospace font !! :D
a rewrite of [pyzule](https://github.com/asdfzxcvbn/pyzule) that doesn't (completely) suck !!
this is pretty much just a hobby project that i'll work on when i'm bored. pyzule in its current state is kinda ugly, but it works and i haven't really had the motivation to work on it. there really isn't any need for improvements, anyway..
`cyan` will ~~target python v3.12 (finally!)~~, be pep8-compliant (hopefully..), and be type-hinted!
wouldn't a go rewrite be really cool? (or in rust or something, adding features to this makes me realize PYTHONM FUCKING SUCKS SOMEONE PLEASE REWRITE IN LIKE ANY COMPILED AND STATICALLY TYPED LANGUAGE PLEASE!!!!!!!
## features
more coming soon! i'm trying to copy pyzule in order to deprecate it in favor of cyan, which is 1000x better
you can open an issue to request a feature :D !! also see my [recommended flags](https://github.com/asdfzxcvbn/pyzule-rw/wiki/recommended-flags)
- generate and use shareable .cyan files to configure IPAs!
- inject deb, dylib, framework, bundle, and appex files/folders
- automatically fix dependencies on CydiaSubstrate **(cyan uses [ElleKit](https://github.com/evelyneee/ellekit/)!)**, Cephei*, and Orion
- copy any unknown file/folder types to app root
- change app name, version, bundle id, and minimum os version
- remove UISupportedDevices
- remove watch app
- fakesign the output ipa/app
- change the app icon
- fakesign the output ipa/tipa/app
- merge a plist into the app's existing Info.plist
- add custom entitlements to the main executable
- thin all binaries to arm64, it can LARGELY reduce app size sometimes!
- remove all app extensions (or just encrypted ones!)
## install instructions
cyan works on linux, macOS, WSL, and jailbroken iOS!
cyan supports **linux, macOS, WSL, and jailbroken iOS!** all either x86_64 or arm64/aarch64 !!
first, make sure you have [ar](https://command-not-found.com/ar) and [tar](https://command-not-found.com/tar) installed
also obviously install python, version 3.9 or greater is required (the version available on the procursus repo for iOS)
also obviously install python, version 3.9 or greater is required
**if you want to inject dylibs AND ARE NOT ON iOS,** make sure you install lief (you only have to do this once): `pip install -U lief`
the `zip` and `unzip` commands are *optional* dependencies, they may [fix issues when extracting certain IPAs with chinese characters](https://github.com/asdfzxcvbn/pyzule-rw/wiki/file-does-not-exist-(executable)-%3F), etc
then finally, to install or update cyan, just `pip install --force-reinstall git+https://github.com/asdfzxcvbn/pyzule-rw.git#egg=cyan`
<details>
<summary><b>linux/WSL/macOS instructions</b></summary>
<br/>
<ol>
<li>install <a href="https://github.com/pypa/pipx?tab=readme-ov-file#install-pipx">pipx</a></li>
<li>install OR update cyan: <code>pipx install --force https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip</code></li>
<li><b>if you want to inject dylibs ON AARCH64 LINUX</b>: <code>pipx inject cyan lief</code></li>
<li><b>if you want to change app icons (iOS NOT supported)</b>: <code>pipx inject cyan Pillow</code></li>
</ol>
</details>
## todo
<details>
<summary><b>jailbroken iOS instructions / automated environment (github workflow, etc)</b></summary>
<br/>
<ol>
<li>install OR update cyan: <code>pip install --force-reinstall https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip</code></li>
</ol>
</details>
[x] refactor: dont prepare, just copy and fix as you go
## making cyan files
[2] feat: plist operations (-n, -v, -b, -m, -l, -r)
cyan comes bundled with the `cgen` command, which lets you generate `.cyan` files to pass to `-z`/`--cyan` !
[x] feat: remove watch app
## acknowledgements
[x] feat: fakesign
[x] feat: thin binaries
[x] feat: plugin operations (-q, -e)
[] feat: .cyan files (lol rip .pyzule files)
- [Al4ise](https://github.com/Al4ise) for the original [Azule](https://github.com/Al4ise/Azule)
- [lief-project](https://github.com/lief-project) for [LIEF](https://github.com/lief-project/LIEF)
- [tyilo](https://github.com/tyilo) for [insert_dylib](https://github.com/tyilo/insert_dylib/) (macOS/iOS)
- [LeanVel](https://github.com/LeanVel) for [insert_dylib](https://github.com/LeanVel/insert_dylib) (linux)

160
cgen/__main__.py Normal file
View File

@@ -0,0 +1,160 @@
import os
import sys
import json
import zipfile
import argparse
def main() -> None:
if sys.platform == "win32":
sys.exit("[!] windows is not supported")
parser = argparse.ArgumentParser(
description="a tool to generate .cyan files"
)
parser.add_argument(
"-o", "--output", metavar="output", required=True,
help="output of the .cyan file"
)
parser.add_argument(
"-f", metavar="file", nargs="+",
help="a tweak to inject/item to be added to the bundle"
)
parser.add_argument(
"-n", metavar="name",
help="modify the app's name"
)
parser.add_argument(
"-v", metavar="version",
help="modify the app's version"
)
parser.add_argument(
"-b", metavar="bundle id",
help="modify the app's bundle id"
)
parser.add_argument(
"-m", metavar="minimum",
help="modify the app's minimum OS version"
)
parser.add_argument(
"-k", metavar="icon",
help="modify the app's icon"
)
parser.add_argument(
"-l", metavar="plist",
help="a plist to merge with the app's Info.plist"
)
parser.add_argument(
"-x", metavar="entitlements",
help="add or modify entitlements to the main binary"
)
parser.add_argument(
"-u", "--remove-supported-devices", action="store_true",
help="remove UISupportedDevices"
)
parser.add_argument(
"-w", "--no-watch", action="store_true",
help="remove all watch apps"
)
parser.add_argument(
"-d", "--enable-documents", action="store_true",
help="enable documents support"
)
parser.add_argument(
"-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(
"-e", "--remove-extensions", action="store_true",
help="remove all app extensions"
)
parser.add_argument(
"-g", "--remove-encrypted", action="store_true",
help="only remove encrypted app extensions"
)
generate_cyan(parser)
def generate_cyan(parser: argparse.ArgumentParser) -> None:
args = parser.parse_args()
# input validation
if args.m is not None and any(c not in "0123456789." for c in args.m):
parser.error(f"invalid minimum OS version: {args.m}")
if args.k is not None and not os.path.isfile(args.k):
parser.error(f"{args.k} does not exist")
if args.l is not None and not os.path.isfile(args.l):
parser.error(f"{args.l} does not exist")
if args.x is not None and not os.path.isfile(args.x):
parser.error(f"{args.x} does not exist")
if args.f is not None:
fake = [f for f in args.f if not os.path.exists(f)]
# it would be great if everyone was using python 3.12 !!!
if len(fake) != 0:
parser.error(f"the following file(s) do not exist: {', '.join(fake)}")
if not args.output.endswith(".cyan"):
print("[*] appended cyan file extension to output")
args.output += ".cyan"
if os.path.isfile(args.output):
try:
overwrite = input(
f"[<] {args.output} already exists. overwrite? [Y/n] "
).lower().strip()
except KeyboardInterrupt:
sys.exit("\n[?] guess not")
if overwrite not in ("y", "yes", ""):
sys.exit("[>] quitting.")
real_args = {k: v for k, v in dict(vars(args)).items() if v}
del real_args["output"]
for key in "fkxl": # these need files
if key in real_args:
real_args[key] = True
print("[*] generating..")
with zipfile.ZipFile(
args.output, "w", zipfile.ZIP_DEFLATED, compresslevel=1
) as zf:
with zf.open("config.json", "w") as f:
f.write(json.dumps(real_args).encode())
if args.f is not None:
for f in args.f:
if os.path.isfile(f):
zf.write(f, f"inject/{os.path.basename(f)}")
else: # G YHUJMNFTGYHNFTGYHTGYHUT6Y7UJM8RFTYHNR564TY
if f.endswith("/"):
f = f[:-1] # yes this is needed to prevent a bug wtf
for dp, _, files in os.walk(f):
for f2 in files:
thing = f"{dp}/{f2}"
zf.write(
thing,
f"inject/{os.path.relpath(thing, os.path.dirname(f))}"
) # no, i don't know what this is doing.
if args.k is not None:
zf.write(args.k, "icon.idk")
if args.l:
zf.write(args.l, "merge.plist")
if args.x:
zf.write(args.x, "new.entitlements")
if __name__ == "__main__":
main()

View File

@@ -22,11 +22,44 @@ def main() -> None:
help="if unspecified, overwrites input"
)
parser.add_argument(
"-z", "--cyan", metavar="cyan", nargs="+",
help="the .cyan file(s) to use"
)
parser.add_argument(
"-f", metavar="file", nargs="+",
help="a tweak to inject/item to be added to the bundle"
)
parser.add_argument(
"-n", metavar="name",
help="modify the app's name"
)
parser.add_argument(
"-v", metavar="version",
help="modify the app's version"
)
parser.add_argument(
"-b", metavar="bundle id",
help="modify the app's bundle id"
)
parser.add_argument(
"-m", metavar="minimum",
help="modify the app's minimum OS version"
)
parser.add_argument(
"-k", metavar="icon",
help="modify the app's icon"
)
parser.add_argument(
"-l", metavar="plist",
help="a plist to merge with the app's Info.plist"
)
parser.add_argument(
"-x", metavar="entitlements",
help="add or modify entitlements to the main binary"
)
parser.add_argument(
"-u", "--remove-supported-devices", action="store_true",
help="remove UISupportedDevices"
@@ -71,7 +104,7 @@ def main() -> None:
)
parser.add_argument(
"-v", "--version", action="version", version="cyan v1.0"
"--version", action="version", version="cyan v1.4.3"
)
from cyan import logic

View File

@@ -4,24 +4,34 @@
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>Cephei</string>
<key>CFBundleIdentifier</key>
<string>ws.hbang.common</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Cephei</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>HBPackageIdentifier</key>
<string>ws.hbang.common</string>
</dict>

View File

@@ -4,26 +4,37 @@
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>CepheiPrefs</string>
<key>CFBundleIdentifier</key>
<string>ws.hbang.common.prefs</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Cephei</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>HBPackageIdentifier</key>
<string>ws.hbang.common</string>
<key>NSPrincipalClass</key>
<string>HBDemoRootListController</string>
</dict>

View File

@@ -12,7 +12,7 @@
<string>Orion</string>
<key>CFBundleShortVersionString</key>
<string>1.0.1</string>
<string>1.0.2</string>
<key>CFBundleVersion</key>
<string>1</string>

Binary file not shown.

View File

@@ -13,8 +13,12 @@ def main(parser: ArgumentParser) -> None:
if args.output is not None:
args.o = os.path.normpath(args.output)
if not (args.o.endswith(".app") or args.o.endswith(".ipa")):
print("[?] output's file extension not specified; will create ipa")
if not (
args.o.endswith(".app")
or args.o.endswith(".ipa")
or args.o.endswith(".tipa")
):
print("[?] valid file extension not found; will create ipa")
args.o += ".ipa"
else:
args.o = args.i
@@ -25,8 +29,9 @@ def main(parser: ArgumentParser) -> None:
if arg_err is not None:
parser.error(arg_err)
INPUT_IS_IPA = True if args.i.endswith(".ipa") else False
OUTPUT_IS_IPA = True if args.o.endswith(".ipa") else False
# mfw when "True if True else False" HAHAHAH
INPUT_IS_IPA = args.i.endswith(".ipa") or args.i.endswith(".tipa")
OUTPUT_IS_IPA = args.o.endswith(".ipa") or args.o.endswith(".tipa")
with TemporaryDirectory() as tmpdir, tbhtypes.LeavingCM():
app_path = tbhutils.get_app(args.i, tmpdir, INPUT_IS_IPA)
@@ -38,6 +43,10 @@ def main(parser: ArgumentParser) -> None:
else:
sys.exit("[!] main binary is encrypted; exiting")
if args.cyan is not None:
changing = vars(args)
tbhutils.parse_cyans(changing, tmpdir)
# this goes before injection,
# since user might inject their own extensions
if args.remove_extensions:
@@ -47,6 +56,20 @@ def main(parser: ArgumentParser) -> None:
if args.f is not None:
app.executable.inject(args.f, tmpdir)
if args.n is not None:
app.plist.change_name(args.n)
if args.v is not None:
app.plist.change_version(args.v)
if args.b is not None:
app.plist.change_bundle_id(args.b)
if args.m is not None:
app.plist.change_minimum_version(args.m)
if args.k is not None:
app.change_icon(args.k, tmpdir)
if args.l is not None:
app.plist.merge_plist(args.l)
if args.x is not None:
app.executable.merge_entitlements(args.x)
if args.remove_supported_devices:
app.plist.remove_uisd()
@@ -72,6 +95,6 @@ def main(parser: ArgumentParser) -> None:
if os.path.isdir(args.o):
shutil.rmtree(args.o)
shutil.move(app_path, args.o)
shutil.move(app.path, args.o)
print(f"[*] generated app at {args.o}")

View File

@@ -1,12 +1,14 @@
from .app_bundle import AppBundle
from .executable import Executable
from .leaving_cm import LeavingCM
from .main_executable import MainExecutable
from .plist import Plist
__all__ = [
"AppBundle",
"Executable",
"LeavingCM",
"MainExecutable",
"Plist"
]

View File

@@ -1,17 +1,19 @@
import os
import shutil
from glob import glob
from uuid import uuid4
from typing import Optional, Literal
from .executable import Executable
from .main_executable import MainExecutable
from .plist import Plist
class AppBundle:
def __init__(self, path: str):
self.path = path
self.plist = Plist(f"{path}/Info.plist")
self.plist = Plist(f"{path}/Info.plist", path)
self.executable = Executable(
self.executable = MainExecutable(
f"{path}/{self.plist['CFBundleExecutable']}",
path
)
@@ -101,3 +103,45 @@ class AppBundle:
else:
print("[*] removed encrypted plugins:", ", ".join(removed))
def change_icon(self, path: str, tmpdir: str) -> None:
try:
from PIL import Image # type: ignore
except Exception:
return print("[?] pillow is not installed, -k is not available")
tmpath = f"{tmpdir}/icon.png"
if not path.endswith(".png"):
with Image.open(path) as img:
img.save(tmpath, "PNG")
else:
shutil.copyfile(path, tmpath)
uid = f"cyan_{uuid4().hex[:7]}a" # can't have it end with a num
i60 = f"{uid}60x60"
i76 = f"{uid}76x76"
with Image.open(tmpath) as img:
img.resize((120, 120)).save(f"{self.path}/{i60}@2x.png", "PNG")
img.resize((152, 152)).save(f"{self.path}/{i76}@2x~ipad.png", "PNG")
if "CFBundleIcons" not in self.plist:
self.plist["CFBundleIcons"] = {}
if "CFBundleIcons~ipad" not in self.plist:
self.plist["CFBundleIcons~ipad"] = {}
self.plist["CFBundleIcons"] = self.plist["CFBundleIcons"] | {
"CFBundlePrimaryIcon": {
"CFBundleIconFiles": [i60],
"CFBundleIconName": uid
}
}
self.plist["CFBundleIcons~ipad"] = self.plist["CFBundleIcons~ipad"] | {
"CFBundlePrimaryIcon": {
"CFBundleIconFiles": [i60, i76],
"CFBundleIconName": uid
}
}
self.plist.save()
print("[*] updated app icon")

View File

@@ -1,13 +1,6 @@
import os
import sys
import shutil
import subprocess
from typing import Optional
try:
import lief
except Exception:
pass
from cyan import tbhutils
@@ -18,34 +11,47 @@ class Executable:
ldid = f"{specific}/ldid"
lipo = f"{specific}/lipo"
otool = f"{specific}/otool"
idylib = f"{specific}/insert_dylib"
starters = ("\t/Library/", "\t@rpath", "\t@executable_path")
common = {
# substrate could show up as
# CydiaSubstrate.framework, libsubstrate.dylib, EVEN CydiaSubstrate.dylib
# AND PROBABLY EVEN MORE !!!! IT'S CRAZY.
"CydiaSubstrate.framework": "CydiaSubstrate.framework",
"Orion.framework": "Orion.framework",
"Cephei.framework": "Cephei.framework",
"CepheiUI.framework": "CepheiUI.framework",
"CepheiPrefs.framework": "CepheiPrefs.framework"
# substrate could show up as
# CydiaSubstrate.framework, libsubstrate.dylib, EVEN CydiaSubstrate.dylib
# AND PROBABLY EVEN MORE !!!! IT'S CRAZY.
common = {
"substrate.": {
"name": "CydiaSubstrate.framework",
"path": "@rpath/CydiaSubstrate.framework/CydiaSubstrate"
},
"orion.": {
"name": "Orion.framework",
"path": "@rpath/Orion.framework/Orion"
},
"cephei.": {
"name": "Cephei.framework",
"path": "@rpath/Cephei.framework/Cephei"
},
"cepheiui.": {
"name": "CepheiUI.framework",
"path": "@rpath/CepheiUI.framework/CepheiUI"
},
"cepheiprefs.": {
"name": "CepheiPrefs.framework",
"path": "@rpath/CepheiPrefs.framework/CepheiPrefs"
}
}
def __init__(self, path: str, bundle_path: Optional[str] = None):
def __init__(self, path: str):
if not os.path.isfile(path):
sys.exit(f"[!] {path} does not exist (executable)")
print(f"[!] {path} does not exist (executable)", file=sys.stderr)
sys.exit(
"[?] check the wiki for info: "
"https://github.com/asdfzxcvbn/pyzule-rw/wiki/"
"file-does-not-exist-(executable)-%3F"
)
self.path = path
self.bundle_path = bundle_path
self.bn = os.path.basename(path)
self.inj: Optional = None # type: ignore
if self.specific.endswith("iOS"):
self.inj_func = self.ios_inject
else:
self.inj_func = self.insert_cmd
def is_encrypted(self) -> bool:
proc = subprocess.run(
@@ -55,112 +61,11 @@ class Executable:
return b"cryptid 1" in proc.stdout
def inject(self, tweaks: dict[str, str], tmpdir: str) -> None:
# we only inject into the main executable
assert self.bundle_path is not None
def remove_signature(self) -> None:
subprocess.run([self.ldid, "-R", self.path], stderr=subprocess.DEVNULL)
has_entitlements = False
ENT_PATH = f"{self.bundle_path}/cyan.entitlements"
PLUGINS_DIR = f"{self.bundle_path}/PlugIns"
FRAMEWORKS_DIR = f"{self.bundle_path}/Frameworks"
with open(ENT_PATH, "wb") as entf:
proc = subprocess.run(
[self.ldid, "-e", self.path],
capture_output=True
)
entf.write(proc.stdout)
if os.path.getsize(ENT_PATH) > 0:
has_entitlements = True
# iirc, injecting doesnt work (sometimes) if the file isn't signed
self.fakesign(False)
if any(t.endswith(".appex") for t in tweaks):
os.makedirs(PLUGINS_DIR, exist_ok=True)
if any(
t.endswith(k)
for t in tweaks
for k in (".deb", ".dylib", ".framework")
):
os.makedirs(FRAMEWORKS_DIR, exist_ok=True)
# some apps really dont have this lol
subprocess.run(
[self.nt, "-add_rpath", "@executable_path/Frameworks", self.path],
stderr=subprocess.DEVNULL
)
# `extract_deb()` will modify `tweaks`, which is why we make a copy
cwd = os.getcwd()
for bn, path in dict(tweaks).items():
if bn.endswith(".deb"):
tbhutils.extract_deb(path, tweaks, tmpdir)
continue
os.chdir(cwd) # i fucking hate jailbroken iOS utils.
needed: set[str] = set()
for bn, path in tweaks.items():
if bn.endswith(".appex"):
fpath = f"{PLUGINS_DIR}/{bn}"
existed = tbhutils.delete_if_exists(fpath, bn)
shutil.copytree(path, fpath)
elif bn.endswith(".dylib"):
path = shutil.copy2(path, tmpdir)
Executable(path).fix_dependencies(tweaks, needed)
fpath = f"{FRAMEWORKS_DIR}/{bn}"
existed = tbhutils.delete_if_exists(fpath, bn)
self.inj_func(f"@rpath/{bn}")
shutil.move(path, FRAMEWORKS_DIR)
elif bn.endswith(".framework"):
fpath = f"{FRAMEWORKS_DIR}/{bn}"
existed = tbhutils.delete_if_exists(fpath, bn)
self.inj_func(f"@rpath/{bn}/{bn[:-10]}")
shutil.copytree(path, fpath)
else:
fpath = f"{self.bundle_path}/{bn}"
existed = tbhutils.delete_if_exists(fpath, bn)
try:
shutil.copytree(path, fpath)
except NotADirectoryError:
shutil.copy2(path, self.bundle_path)
if not existed:
print(f"[*] injected {bn}")
# orion has a *weak* dependency to substrate,
# but will still crash without it. nice !!!!!!!!!!!
if "Orion.framework" in needed:
needed.add("CydiaSubstrate.framework")
for missing in needed:
real = self.common[missing] # "real" name, thanks substrate!
ip = f"{FRAMEWORKS_DIR}/{real}"
existed = tbhutils.delete_if_exists(ip, real)
shutil.copytree(f"{self.install_dir}/extras/{real}", ip)
if not existed:
print(f"[*] auto-injected {real}")
# FINALLY !!
if self.inj is not None: # type: ignore
self.inj.write(self.path) # type: ignore
if has_entitlements:
subprocess.run(["ldid", f"-S{ENT_PATH}", self.path])
print("[*] restored entitlements")
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 fakesign(self) -> bool:
return subprocess.run([self.ldid, "-S", "-M", self.path]).returncode == 0
def thin(self) -> bool:
return subprocess.run(
@@ -174,34 +79,29 @@ class Executable:
stderr=subprocess.DEVNULL
)
def insert_cmd(self, cmd: str) -> None:
if self.inj is None: # type: ignore
try:
lief.logging.disable() # type: ignore
except Exception:
sys.exit("[!] did you forget to install lief?")
self.inj = lief.parse(self.path) # type: ignore
self.inj.add(lief.MachO.DylibCommand.weak_lib(cmd)) # type: ignore
def ios_inject(self, cmd: str) -> None:
subprocess.run(
[
f"{self.specific}/insert_dylib",
"--weak", "--inplace", "--no-strip-codesig", "--all-yes",
cmd, self.path
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
def fix_dependencies(self, tweaks: dict[str, str], need: set[str]) -> None:
self.fakesign() # sometimes it doesnt work if we dont do this
def fix_common_dependencies(self, needed: set[str]) -> None:
self.remove_signature()
for dep in self.get_dependencies():
for cname in (tweaks | self.common):
for common, info in self.common.items():
if common in dep.lower():
needed.add(common)
if dep != info["path"]:
self.change_dependency(dep, info["path"])
print(
f"[*] fixed common dependency in {self.bn}: "
f"{dep} -> {info['path']}"
)
def fix_dependencies(self, tweaks: dict[str, str]) -> None:
for dep in self.get_dependencies():
for cname in tweaks:
if cname in dep:
# i wonder if there's a better way to do this?
if cname.endswith(".framework"):
# nah, not gonna parse the plist,
# i've never seen a framework with a "mismatched" name
npath = f"@rpath/{cname}/{cname[:-10]}"
else:
npath = f"@rpath/{cname}"
@@ -210,9 +110,6 @@ class Executable:
self.change_dependency(dep, npath)
print(f"[*] fixed dependency in {self.bn}: {dep} -> {npath}")
if cname in self.common:
need.add(cname)
def get_dependencies(self) -> list[str]:
proc = subprocess.run(
[self.otool, "-L", self.path],

View File

@@ -0,0 +1,168 @@
import os
import sys
import shutil
import subprocess
from typing import Optional
try:
import lief # type: ignore
except Exception:
pass
from cyan import tbhutils
from .executable import Executable
class MainExecutable(Executable):
def __init__(self, path: str, bundle_path: str):
super().__init__(path)
self.bundle_path = bundle_path
self.inj: Optional = None # type: ignore
if os.path.isfile(self.idylib):
self.inj_func = self.idyl_inject
else:
self.inj_func = self.lief_inject
def inject(self, tweaks: dict[str, str], tmpdir: str) -> None:
ENT_PATH = f"{self.bundle_path}/cyan.entitlements"
PLUGINS_DIR = f"{self.bundle_path}/PlugIns"
FRAMEWORKS_DIR = f"{self.bundle_path}/Frameworks"
has_entitlements = self.write_entitlements(ENT_PATH)
# iirc, injecting doesnt work (sometimes) if the file is signed
self.remove_signature()
if any(t.endswith(".appex") for t in tweaks):
os.makedirs(PLUGINS_DIR, exist_ok=True)
if any(
t.endswith(k)
for t in tweaks
for k in (".deb", ".dylib", ".framework")
):
os.makedirs(FRAMEWORKS_DIR, exist_ok=True)
# some apps really dont have this lol
subprocess.run(
[self.nt, "-add_rpath", "@executable_path/Frameworks", self.path],
stderr=subprocess.DEVNULL
)
# `extract_deb()` will modify `tweaks`, which is why we make a copy
cwd = os.getcwd()
for bn, path in dict(tweaks).items():
if bn.endswith(".deb"):
tbhutils.extract_deb(path, tweaks, tmpdir)
continue
os.chdir(cwd) # i fucking hate jailbroken iOS utils.
needed: set[str] = set()
# inject/fix user things
for bn, path in tweaks.items():
if os.path.islink(path):
continue # symlinks can potentially have some security implications
if bn.endswith(".appex"):
fpath = f"{PLUGINS_DIR}/{bn}"
existed = tbhutils.delete_if_exists(fpath, bn)
shutil.copytree(path, fpath)
elif bn.endswith(".dylib"):
path = shutil.copy2(path, tmpdir)
e = Executable(path)
e.fix_common_dependencies(needed)
e.fix_dependencies(tweaks)
fpath = f"{FRAMEWORKS_DIR}/{bn}"
existed = tbhutils.delete_if_exists(fpath, bn)
self.inj_func(f"@rpath/{bn}")
shutil.move(path, FRAMEWORKS_DIR)
elif bn.endswith(".framework"):
fpath = f"{FRAMEWORKS_DIR}/{bn}"
existed = tbhutils.delete_if_exists(fpath, bn)
self.inj_func(f"@rpath/{bn}/{bn[:-10]}")
shutil.copytree(path, fpath)
else:
fpath = f"{self.bundle_path}/{bn}"
existed = tbhutils.delete_if_exists(fpath, bn)
try:
shutil.copytree(path, fpath)
except NotADirectoryError:
shutil.copy2(path, self.bundle_path)
if not existed:
print(f"[*] injected {bn}")
# orion has a *weak* dependency to substrate,
# but will still crash without it. nice !!!!!!!!!!!
## edit: actually, maybe this is in case someone uses Internal backend?
## someone test it pls!!!
if "orion." in needed:
needed.add("substrate.")
for missing in needed:
real = self.common[missing]["name"] # e.g. "Orion.framework"
ip = f"{FRAMEWORKS_DIR}/{real}"
existed = tbhutils.delete_if_exists(ip, real)
shutil.copytree(f"{self.install_dir}/extras/{real}", ip)
if not existed:
print(f"[*] auto-injected {real}")
# FINALLY !!
if self.inj is not None: # type: ignore
self.inj.write(self.path) # type: ignore
if has_entitlements:
self.sign_with_entitlements(ENT_PATH)
print("[*] restored entitlements")
def write_entitlements(self, output: str) -> bool:
with open(output, "wb") as entf:
proc = subprocess.run(
[self.ldid, "-e", self.path],
capture_output=True
)
entf.write(proc.stdout)
return os.path.getsize(output) > 0
def merge_entitlements(self, entitlements: str) -> None:
if self.sign_with_entitlements(entitlements):
print("[*] merged new entitlements")
else:
print("[!] failed to merge new entitlements, are they valid?")
def sign_with_entitlements(self, entitlements: str) -> bool:
return subprocess.run(
[self.ldid, f"-S{entitlements}", "-M", "-Cadhoc", self.path]
).returncode == 0
def lief_inject(self, cmd: str) -> None:
if self.inj is None: # type: ignore
try:
lief.logging.disable() # type: ignore
except Exception:
sys.exit("[!] did you forget to install lief?")
self.inj = lief.parse(self.path) # type: ignore
try:
self.inj.add(lief.MachO.DylibCommand.weak_lib(cmd)) # type: ignore
except AttributeError:
sys.exit("[!] couldn't add LC (lief), did you use a valid app?")
def idyl_inject(self, cmd: str) -> None:
proc = subprocess.run(
[
self.idylib, "--weak", "--inplace", "--all-yes",
cmd, self.path
], capture_output=True, text=True
)
if proc.returncode != 0:
sys.exit(f"[!] couldn't add LC (insert_dylib), error:\n{proc.stderr}")

View File

@@ -1,16 +1,25 @@
import sys
import plistlib
from typing import Any
from glob import glob
from typing import Optional, Any
class Plist:
def __init__(self, path: str):
def __init__(
self, path: str, app_path: Optional[str] = None, throw: bool = True
):
try:
with open(path, "rb") as f:
self.data: dict[str, Any] = plistlib.load(f)
self.success = True
except Exception:
sys.exit(f"[!] couldn't read {path}")
if throw:
sys.exit(f"[!] couldn't read {path}")
self.success = False
self.path = path
self.app_path = app_path
def __getitem__(self, key: str) -> Any:
return self.data.get(key, None)
@@ -18,6 +27,9 @@ class Plist:
def __setitem__(self, key: str, val: Any) -> None:
self.data[key] = val
def __contains__(self, key: str) -> bool:
return key in self.data
def save(self) -> None:
with open(self.path, "wb") as f:
plistlib.dump(self.data, f)
@@ -25,33 +37,104 @@ class Plist:
def remove(self, key: str) -> bool:
try:
del self.data[key]
self.save()
return True
except KeyError:
return False
def change(self, key: str, val: Any) -> bool:
def change(self, val: Any, *keys: str) -> bool:
try:
if self[key] == val:
if all(self[key] == val for key in keys):
return False
raise KeyError
except KeyError:
self[key] = val
for key in keys:
self[key] = val
self.save()
return True
def remove_uisd(self) -> None:
if self.remove("UISupportedDevices"):
self.save()
print("[*] removed UISupportedDevices")
else:
print("[?] no UISupportedDevices")
def enable_documents(self) -> None:
c1 = self.change("UISupportsDocumentBrowser", True)
c2 = self.change("UIFileSharingEnabled", True)
c1 = self.change(True, "UISupportsDocumentBrowser")
c2 = self.change(True, "UIFileSharingEnabled")
if c1 or c2:
self.save()
print("[*] enabled documents support")
else:
print("[?] documents support was already enabled")
def change_name(self, name: str) -> None:
if self.change(name, "CFBundleName", "CFBundleDisplayName"):
print(f"[*] changed name to \"{name}\"")
changed = 0
for lproj in glob(f"{self.app_path}/*.lproj"):
try:
pl = Plist(f"{lproj}/InfoPlist.strings", None, False)
pl.change(name, "CFBundleName", "CFBundleDisplayName")
pl.save()
changed += 1
except Exception:
pass # file might not exist
if changed != 0:
print(f"[*] changed \033[96m{changed}\033[0m localized names")
else:
print(f"[?] name was already \"{name}\"")
def change_version(self, version: str) -> None:
if self.change(version, "CFBundleVersion", "CFBundleShortVersionString"):
print(f"[*] changed version to \"{version}\"")
else:
print(f"[?] version was already \"{version}\"")
def change_bundle_id(self, bundle_id: str) -> None:
orig = self["CFBundleIdentifier"]
if self.change(bundle_id, "CFBundleIdentifier"):
print(f"[*] changed bundle id to \"{bundle_id}\"")
changed = 0
# change all other bundle ids
for ext in glob(f"{self.app_path}/*/*.appex"):
try:
pl = Plist(f"{ext}/Info.plist", None, False)
current = pl["CFBundleIdentifier"]
pl["CFBundleIdentifier"] = current.replace(orig, bundle_id)
pl.save()
changed += 1
except Exception:
pass # how tf would it not exist? idk
if changed != 0:
print(f"[*] changed \033[96m{changed}\033[0m other bundle ids")
else:
print(f"[?] bundle id was already \"{bundle_id}\"")
def change_minimum_version(self, minimum: str) -> None:
if self.change(minimum, "MinimumOSVersion"):
print(f"[*] changed minimum version to \"{minimum}\"")
else:
print(f"[?] minimum version was already \"{minimum}\"")
def merge_plist(self, path: str) -> None:
pl = Plist(path, throw=False)
if not pl.success:
return print(f"[!] couldn't parse {path}")
changed = False
for k, v in pl.data.items():
if self.change(v, k):
changed = True
if not changed:
print("[?] no modified plist entries")
else:
print("[*] set plist keys:", ", ".join(pl.data))

View File

@@ -1,21 +1,27 @@
import os
import sys
import json
import shutil
import zipfile
import platform
import subprocess
from uuid import uuid4
from typing import Optional
from glob import glob, iglob
from argparse import Namespace
from typing import Optional, Any
from plistlib import load as pload
HAS_ZIP = shutil.which("zip") is not None
HAS_UNZIP = shutil.which("unzip") is not None
def validate_inputs(args: Namespace) -> Optional[str]:
if not (
args.i.endswith(".ipa")
or args.i.endswith(".app")
args.i.endswith(".app")
or args.i.endswith(".ipa")
or args.i.endswith(".tipa")
):
return "the input file must be an ipa/app"
return "the input file must be an ipa/tipa/app"
if not os.path.exists(args.i):
return f"{args.i} does not exist"
@@ -38,15 +44,46 @@ def validate_inputs(args: Namespace) -> Optional[str]:
sys.exit(0)
if args.f is not None:
# dictionary ensures unique names
args.f = {os.path.basename(f): os.path.realpath(f) for f in args.f}
nonexistent = [f for f in args.f.values() if not os.path.exists(f)]
new: dict[str, str] = {} # dictionary ensures unique names
if len(nonexistent) != 0:
print("[!] please ensure the following file(s) exist:")
for ne in nonexistent:
print(f"[?] - {ne}")
sys.exit(1)
for f in list(args.f):
if f[-1] == "/": # yeah this is stupid
f = f[:-1]
if not os.path.exists(f):
sys.exit(f"[!] \"{f}\" does not exist")
new[os.path.basename(f)] = os.path.realpath(f)
# i would've modified args.f directly, but it causes type-hinting error :(
args.f = new
if (
args.m is not None
and any(char not in "0123456789." for char in args.m)
):
sys.exit(f"[!] invalid OS version: {args.m}")
if args.k is not None and not os.path.isfile(args.k):
sys.exit(f"[!] {args.k} does not exist")
if args.l is not None and not os.path.isfile(args.l):
sys.exit(f"[!] {args.l} does not exist")
if args.cyan is not None:
for cyan in args.cyan:
if not os.path.isfile(cyan):
sys.exit(f"[!] {cyan} does not exist")
if args.x is not None:
if not os.path.isfile(args.x):
sys.exit(f"[!] {args.x} does not exist")
try:
with open(args.x, "rb") as f:
pload(f)
except Exception:
sys.exit("[!] couldn't parse given entitlements file")
def get_app(path: str, tmpdir: str, is_ipa: bool) -> str:
@@ -64,7 +101,15 @@ def get_app(path: str, tmpdir: str, is_ipa: bool) -> str:
elif not any(name.endswith(".app/Info.plist") for name in names):
sys.exit("[!] no Info.plist, invalid app")
ipa.extractall(tmpdir)
# using unzip fixes extraction errors in ipas with chinese chars, etc
if HAS_UNZIP:
subprocess.run(
["unzip", path, "-d", tmpdir],
stdout=subprocess.DEVNULL
)
else:
ipa.extractall(tmpdir)
app = glob(f"{payload}/*.app")[0]
except (KeyError, IndexError):
sys.exit("[!] couldn't find either Payload or app folder, invalid ipa")
@@ -93,11 +138,12 @@ def get_tools_dir() -> tuple[str, str]:
# thank god i dont have to use importlib,
# it's the WORST standard library package prior to 3.12
install_dir = os.path.dirname(__file__)
specific_dir = f"{install_dir}/tools/{system}/{mach}"
return (
install_dir,
f"{install_dir}/tools/{system}/{mach}"
)
if not os.path.isdir(specific_dir):
sys.exit(f"[!] cyan is not supported on: {system} {mach}")
return (install_dir, specific_dir)
def delete_if_exists(path: str, bn: str) -> bool:
@@ -140,12 +186,16 @@ def extract_deb(deb: str, tweaks: dict[str, str], tmpdir: str) -> None:
for hi in sum((
glob(f"{t2}/**/*.dylib", recursive=True),
glob(f"{t2}/**/*.bundle", recursive=True),
glob(f"{t2}/**/*.appex", recursive=True),
glob(f"{t2}/**/*.bundle", recursive=True),
glob(f"{t2}/**/*.framework", recursive=True)
), []): # type: ignore
if os.path.islink(hi):
continue # symlinks are broken iirc
if (
os.path.islink(hi) # symlinks are broken iirc, also for security
or hi.count(".bundle") > 1 # prevent sub-bundle detection (rip)
or hi.count(".framework") > 1
):
continue
tweaks[os.path.basename(hi)] = hi
@@ -158,15 +208,63 @@ def make_ipa(tmpdir: str, output: str, level: int) -> None:
os.chdir(tmpdir)
weird = 0
with zipfile.ZipFile(
output, "w", zipfile.ZIP_DEFLATED, compresslevel=level
) as zf:
for f in iglob("Payload/**", recursive=True):
try:
zf.write(f)
except ValueError:
weird += 1
if HAS_ZIP:
try:
os.remove(output) # zip command updates zipfiles by default
except FileNotFoundError:
pass
# don't zip hidden files to fix an installd error sometimes
# thanks a lot eevee 😭
subprocess.run(
["zip", f"-{level}", "-r", output, "Payload", "-x", "*/.*"],
stdout=subprocess.DEVNULL
)
else:
with zipfile.ZipFile(
output, "w", zipfile.ZIP_DEFLATED, compresslevel=level
) as zf:
for f in iglob("Payload/**", recursive=True):
try:
zf.write(f)
except ValueError:
weird += 1
if weird != 0:
print(f"[?] was unable to zip {weird} file(s) due to timestamps")
def parse_cyans(args: dict[str, Any], tmpdir: str) -> None:
for ind, cyan in enumerate(args["cyan"]):
print(f"[*] parsing {os.path.basename(cyan)} ..")
with zipfile.ZipFile(cyan) as zf:
DOT_PATH = f"{tmpdir}/cyan-{ind}"
os.mkdir(DOT_PATH)
with zf.open("config.json") as f:
config = json.load(f)
if "f" in config:
NAMES = [n for n in zf.namelist() if n.startswith("inject/")]
zf.extractall(DOT_PATH, NAMES)
# ensure not None
args["f"] = args["f"] if args["f"] is not None else {}
for e in os.scandir(f"{DOT_PATH}/inject"):
args["f"][e.name] = e.path
del config["f"]
if "k" in config:
args["k"] = zf.extract("icon.idk", DOT_PATH)
del config["k"]
if "l" in config:
args["l"] = zf.extract("merge.plist", DOT_PATH)
del config["l"]
if "x" in config:
args["x"] = zf.extract("new.entitlements", DOT_PATH)
del config["x"]
# the rest of the config (not the ones above, we `del` them)
for k, v in config.items():
args[k] = v

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,17 +1,21 @@
# type: ignore
from setuptools import setup
setup(
name="cyan",
version="1.0",
version="1.4.3",
description="finally, pyzule doesn't suck",
author="zx",
author_email="hi@zxcvbn.fyi",
packages=["cyan", "cyan.tbhtypes"],
# install_requires=["lief"],
author_email="zx@hrzn.email",
packages=["cyan", "cyan.tbhtypes", "cgen"],
python_requires=">=3.9",
include_package_data=True,
entry_points={
"console_scripts": ["cyan=cyan.__main__:main"],
"console_scripts": [
"cyan=cyan.__main__:main",
"cgen=cgen.__main__:main"
],
}
)