pyrph 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. pyrph-1.0.0/PKG-INFO +10 -0
  2. pyrph-1.0.0/api/__init__.py +0 -0
  3. pyrph-1.0.0/api/database.py +35 -0
  4. pyrph-1.0.0/api/main.py +234 -0
  5. pyrph-1.0.0/api/utils.py +21 -0
  6. pyrph-1.0.0/pyrph/__init__.py +2 -0
  7. pyrph-1.0.0/pyrph/__main__.py +257 -0
  8. pyrph-1.0.0/pyrph/bot.py +186 -0
  9. pyrph-1.0.0/pyrph/cli.py +170 -0
  10. pyrph-1.0.0/pyrph/core/__init__.py +4 -0
  11. pyrph-1.0.0/pyrph/core/base.py +28 -0
  12. pyrph-1.0.0/pyrph/core/pipeline.py +38 -0
  13. pyrph-1.0.0/pyrph/core/result.py +22 -0
  14. pyrph-1.0.0/pyrph/crypto/__init__.py +7 -0
  15. pyrph-1.0.0/pyrph/crypto/env_bind.py +191 -0
  16. pyrph-1.0.0/pyrph/crypto/keygen.py +88 -0
  17. pyrph-1.0.0/pyrph/key/__init__.py +3 -0
  18. pyrph-1.0.0/pyrph/key/client.py +148 -0
  19. pyrph-1.0.0/pyrph/key/hwid.py +35 -0
  20. pyrph-1.0.0/pyrph/native/__init__.py +4 -0
  21. pyrph-1.0.0/pyrph/native/builder.py +185 -0
  22. pyrph-1.0.0/pyrph/native/wb_aes.py +96 -0
  23. pyrph-1.0.0/pyrph/phases/__init__.py +2 -0
  24. pyrph-1.0.0/pyrph/phases/unified.py +252 -0
  25. pyrph-1.0.0/pyrph/transforms/__init__.py +23 -0
  26. pyrph-1.0.0/pyrph/transforms/anti_debug.py +241 -0
  27. pyrph-1.0.0/pyrph/transforms/anti_dump.py +165 -0
  28. pyrph-1.0.0/pyrph/transforms/cff.py +149 -0
  29. pyrph-1.0.0/pyrph/transforms/chaos.py +145 -0
  30. pyrph-1.0.0/pyrph/transforms/dead_code.py +103 -0
  31. pyrph-1.0.0/pyrph/transforms/expr_explode.py +161 -0
  32. pyrph-1.0.0/pyrph/transforms/import_obf.py +78 -0
  33. pyrph-1.0.0/pyrph/transforms/junk.py +83 -0
  34. pyrph-1.0.0/pyrph/transforms/mba.py +197 -0
  35. pyrph-1.0.0/pyrph/transforms/native_pack.py +225 -0
  36. pyrph-1.0.0/pyrph/transforms/number_enc.py +96 -0
  37. pyrph-1.0.0/pyrph/transforms/opaque.py +137 -0
  38. pyrph-1.0.0/pyrph/transforms/rename.py +204 -0
  39. pyrph-1.0.0/pyrph/transforms/self_mutate.py +124 -0
  40. pyrph-1.0.0/pyrph/transforms/string_vault.py +176 -0
  41. pyrph-1.0.0/pyrph/transforms/strip.py +53 -0
  42. pyrph-1.0.0/pyrph/vm/__init__.py +11 -0
  43. pyrph-1.0.0/pyrph/vm/compiler.py +607 -0
  44. pyrph-1.0.0/pyrph/vm/encryptor.py +209 -0
  45. pyrph-1.0.0/pyrph/vm/opcodes.py +168 -0
  46. pyrph-1.0.0/pyrph/vm/poly_vm_gen.py +342 -0
  47. pyrph-1.0.0/pyrph.egg-info/PKG-INFO +10 -0
  48. pyrph-1.0.0/pyrph.egg-info/SOURCES.txt +52 -0
  49. pyrph-1.0.0/pyrph.egg-info/dependency_links.txt +1 -0
  50. pyrph-1.0.0/pyrph.egg-info/entry_points.txt +2 -0
  51. pyrph-1.0.0/pyrph.egg-info/requires.txt +2 -0
  52. pyrph-1.0.0/pyrph.egg-info/top_level.txt +2 -0
  53. pyrph-1.0.0/setup.cfg +4 -0
  54. pyrph-1.0.0/setup.py +24 -0
pyrph-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.1
2
+ Name: pyrph
3
+ Version: 1.0.0
4
+ Summary: Python Obfuscation Engine — VM + Native + MBA
5
+ Author: Therealtobu
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Operating System :: OS Independent
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: requests>=2.31.0
10
+ Requires-Dist: python-dotenv>=1.0.0
File without changes
@@ -0,0 +1,35 @@
1
+ import os
2
+ from datetime import datetime
3
+ from sqlalchemy import create_engine, Column, String, Boolean, DateTime, Integer, Text
4
+ from sqlalchemy.orm import declarative_base, sessionmaker
5
+
6
+ DB_PATH = os.environ.get("DB_PATH", "pyrph.db")
7
+ engine = create_engine(f"sqlite:///{DB_PATH}", connect_args={"check_same_thread": False})
8
+ Base = declarative_base()
9
+ Session = sessionmaker(bind=engine)
10
+
11
+
12
+ class Key(Base):
13
+ __tablename__ = "keys"
14
+ id = Column(Integer, primary_key=True, autoincrement=True)
15
+ key_string = Column(String(64), unique=True, nullable=False, index=True)
16
+ hwid = Column(String(128), nullable=True, index=True)
17
+ tier = Column(String(16), default="free") # free | paid
18
+ is_active = Column(Boolean, default=True)
19
+ is_used = Column(Boolean, default=False) # free one-shot flag
20
+ created_at = Column(DateTime, default=datetime.utcnow)
21
+ expires_at = Column(DateTime, nullable=True)
22
+ note = Column(Text, nullable=True)
23
+ used_count = Column(Integer, default=0)
24
+
25
+
26
+ def init_db():
27
+ Base.metadata.create_all(engine)
28
+
29
+
30
+ def get_db():
31
+ db = Session()
32
+ try:
33
+ yield db
34
+ finally:
35
+ db.close()
@@ -0,0 +1,234 @@
1
+ import os
2
+ from datetime import datetime
3
+ from typing import Optional
4
+ from fastapi import FastAPI, Depends, HTTPException, Request
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from fastapi.responses import HTMLResponse, FileResponse
7
+ from fastapi.staticfiles import StaticFiles
8
+ from pydantic import BaseModel
9
+ from sqlalchemy.orm import Session
10
+
11
+ from .database import init_db, get_db, Key
12
+ from .utils import gen_key, hash_hwid, is_admin, free_expiry, is_expired
13
+
14
+ app = FastAPI(docs_url=None, redoc_url=None)
15
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
16
+
17
+ _web = os.path.join(os.path.dirname(__file__), "..", "web", "static")
18
+ if os.path.exists(_web):
19
+ app.mount("/static", StaticFiles(directory=_web), name="static")
20
+
21
+ @app.on_event("startup")
22
+ def startup(): init_db()
23
+
24
+ # ── Models ────────────────────────────────────────────────────────────────
25
+ class GetkeyReq(BaseModel):
26
+ hwid: str
27
+
28
+ class VerifyReq(BaseModel):
29
+ key: str
30
+ hwid: str
31
+
32
+ class ActivateReq(BaseModel):
33
+ key: str
34
+ hwid: str
35
+
36
+ class MarkUsedReq(BaseModel):
37
+ key: str
38
+ hwid: str
39
+
40
+ class AdminGenReq(BaseModel):
41
+ token: str
42
+ tier: str = "paid"
43
+ note: Optional[str] = None
44
+
45
+ class AdminRevokeReq(BaseModel):
46
+ token: str
47
+ key: str
48
+
49
+ class AdminActivateReq(BaseModel):
50
+ token: str
51
+ hwid: str # hashed HWID from user
52
+ key: str
53
+
54
+ class AdminResetReq(BaseModel):
55
+ token: str
56
+ key: str
57
+
58
+
59
+ # ── Public ────────────────────────────────────────────────────────────────
60
+
61
+ @app.post("/api/getkey")
62
+ async def getkey(req: GetkeyReq, db: Session = Depends(get_db)):
63
+ hashed = hash_hwid(req.hwid)
64
+
65
+ # Check existing active free key
66
+ existing = db.query(Key).filter(
67
+ Key.hwid == hashed, Key.tier == "free",
68
+ Key.is_active == True, Key.is_used == False
69
+ ).first()
70
+
71
+ if existing and not is_expired(existing.expires_at):
72
+ return {"ok": True, "key": existing.key_string,
73
+ "tier": "free", "message": "You already have an active free key."}
74
+
75
+ # Create new free key (not yet bound to HWID — bound on activate)
76
+ k = Key(key_string=gen_key("free"), tier="free",
77
+ expires_at=free_expiry(), is_used=False)
78
+ db.add(k); db.commit()
79
+ return {"ok": True, "key": k.key_string, "tier": "free",
80
+ "expires_at": k.expires_at.isoformat(),
81
+ "message": "Free key generated. Activate it with: pyrph --activate <key>"}
82
+
83
+
84
+ @app.post("/api/activate")
85
+ async def activate(req: ActivateReq, db: Session = Depends(get_db)):
86
+ hashed = hash_hwid(req.hwid)
87
+ k = db.query(Key).filter(Key.key_string == req.key).first()
88
+
89
+ if not k:
90
+ raise HTTPException(403, "Invalid key.")
91
+ if not k.is_active:
92
+ raise HTTPException(403, "Key revoked.")
93
+ if is_expired(k.expires_at):
94
+ k.is_active = False; db.commit()
95
+ raise HTTPException(403, "Key expired.")
96
+ if k.is_used and k.tier == "free":
97
+ raise HTTPException(403, "Free key already used. Get a new one.")
98
+
99
+ # Bind HWID if not yet bound
100
+ if k.hwid is None:
101
+ k.hwid = hashed; db.commit()
102
+ elif k.hwid != hashed:
103
+ raise HTTPException(403, "Key bound to a different machine.")
104
+
105
+ return {"ok": True, "tier": k.tier,
106
+ "expires_at": k.expires_at.isoformat() if k.expires_at else None}
107
+
108
+
109
+ @app.post("/api/verify")
110
+ async def verify(req: VerifyReq, db: Session = Depends(get_db)):
111
+ hashed = hash_hwid(req.hwid)
112
+ k = db.query(Key).filter(Key.key_string == req.key).first()
113
+
114
+ if not k: raise HTTPException(403, "Invalid key.")
115
+ if not k.is_active: raise HTTPException(403, "Key revoked.")
116
+ if is_expired(k.expires_at):
117
+ k.is_active = False; db.commit()
118
+ raise HTTPException(403, "Key expired.")
119
+ if k.is_used and k.tier == "free":
120
+ raise HTTPException(403, "Free key already used.")
121
+ if k.hwid and k.hwid != hashed:
122
+ raise HTTPException(403, "Key bound to a different machine.")
123
+
124
+ k.used_count += 1; db.commit()
125
+
126
+ features = {
127
+ "profiles": ["fast","balanced"] if k.tier=="free" else ["fast","balanced","max","stealth","vm","vm_max"],
128
+ "native": k.tier == "paid",
129
+ "nested_vm": k.tier == "paid",
130
+ "poly_vm": True,
131
+ "one_shot": k.tier == "free",
132
+ }
133
+ return {"ok": True, "tier": k.tier, "features": features,
134
+ "expires_at": k.expires_at.isoformat() if k.expires_at else None}
135
+
136
+
137
+ @app.post("/api/mark_used")
138
+ async def mark_used(req: MarkUsedReq, db: Session = Depends(get_db)):
139
+ hashed = hash_hwid(req.hwid)
140
+ k = db.query(Key).filter(Key.key_string == req.key, Key.hwid == hashed).first()
141
+ if k and k.tier == "free":
142
+ k.is_used = True; db.commit()
143
+ return {"ok": True}
144
+
145
+
146
+ @app.get("/api/status")
147
+ async def status():
148
+ return {"status": "online", "version": "1.0.0"}
149
+
150
+
151
+ # ── Admin ─────────────────────────────────────────────────────────────────
152
+
153
+ @app.post("/api/admin/genkey")
154
+ async def admin_genkey(req: AdminGenReq, db: Session = Depends(get_db)):
155
+ if not is_admin(req.token): raise HTTPException(401, "Unauthorized.")
156
+ tier = req.tier if req.tier in ("free","paid") else "paid"
157
+ k = Key(key_string=gen_key(tier), tier=tier,
158
+ expires_at=free_expiry() if tier=="free" else None,
159
+ note=req.note)
160
+ db.add(k); db.commit()
161
+ return {"ok": True, "key": k.key_string, "tier": tier,
162
+ "expires_at": k.expires_at.isoformat() if k.expires_at else "lifetime"}
163
+
164
+
165
+ @app.post("/api/admin/activate")
166
+ async def admin_activate(req: AdminActivateReq, db: Session = Depends(get_db)):
167
+ """Admin manually binds a paid key to a user's HWID."""
168
+ if not is_admin(req.token): raise HTTPException(401, "Unauthorized.")
169
+ k = db.query(Key).filter(Key.key_string == req.key).first()
170
+ if not k: raise HTTPException(404, "Key not found.")
171
+ k.hwid = req.hwid # already hashed from client
172
+ k.is_active = True
173
+ db.commit()
174
+ return {"ok": True, "message": f"Key {req.key} bound to HWID."}
175
+
176
+
177
+ @app.post("/api/admin/reset_hwid")
178
+ async def admin_reset_hwid(req: AdminResetReq, db: Session = Depends(get_db)):
179
+ """Reset HWID binding so key can be activated on a new machine."""
180
+ if not is_admin(req.token): raise HTTPException(401, "Unauthorized.")
181
+ k = db.query(Key).filter(Key.key_string == req.key).first()
182
+ if not k: raise HTTPException(404, "Key not found.")
183
+ k.hwid = None; db.commit()
184
+ return {"ok": True, "message": "HWID reset. User can activate on a new machine."}
185
+
186
+
187
+ @app.post("/api/admin/revoke")
188
+ async def admin_revoke(req: AdminRevokeReq, db: Session = Depends(get_db)):
189
+ if not is_admin(req.token): raise HTTPException(401, "Unauthorized.")
190
+ k = db.query(Key).filter(Key.key_string == req.key).first()
191
+ if not k: raise HTTPException(404, "Key not found.")
192
+ k.is_active = False; db.commit()
193
+ return {"ok": True, "message": f"Key {req.key} revoked."}
194
+
195
+
196
+ @app.get("/api/admin/keys")
197
+ async def admin_keys(token: str, db: Session = Depends(get_db)):
198
+ if not is_admin(token): raise HTTPException(401, "Unauthorized.")
199
+ keys = db.query(Key).order_by(Key.created_at.desc()).limit(300).all()
200
+ return {"ok": True, "keys": [
201
+ {"key": k.key_string, "tier": k.tier,
202
+ "hwid": (k.hwid[:12]+"..." if k.hwid else None),
203
+ "hwid_full": k.hwid,
204
+ "active": k.is_active, "used": k.is_used,
205
+ "uses": k.used_count, "note": k.note,
206
+ "expires": k.expires_at.isoformat() if k.expires_at else "lifetime",
207
+ "created": k.created_at.isoformat()}
208
+ for k in keys
209
+ ]}
210
+
211
+
212
+ @app.get("/api/admin/stats")
213
+ async def admin_stats(token: str, db: Session = Depends(get_db)):
214
+ if not is_admin(token): raise HTTPException(401, "Unauthorized.")
215
+ total = db.query(Key).count()
216
+ active = db.query(Key).filter(Key.is_active==True).count()
217
+ free = db.query(Key).filter(Key.tier=="free", Key.is_active==True).count()
218
+ paid = db.query(Key).filter(Key.tier=="paid", Key.is_active==True).count()
219
+ uses = sum(k.used_count for k in db.query(Key).all())
220
+ return {"ok":True,"total_keys":total,"active_keys":active,
221
+ "free_keys":free,"paid_keys":paid,"total_uses":uses}
222
+
223
+
224
+ # ── Pages ─────────────────────────────────────────────────────────────────
225
+ _T = lambda f: os.path.join(os.path.dirname(__file__),"..","web","templates",f)
226
+
227
+ @app.get("/", response_class=HTMLResponse)
228
+ async def pg_index(): return FileResponse(_T("index.html"))
229
+
230
+ @app.get("/getkey", response_class=HTMLResponse)
231
+ async def pg_getkey(): return FileResponse(_T("getkey.html"))
232
+
233
+ @app.get("/admin", response_class=HTMLResponse)
234
+ async def pg_admin(): return FileResponse(_T("admin.html"))
@@ -0,0 +1,21 @@
1
+ import hashlib, hmac, os, secrets
2
+ from datetime import datetime, timedelta
3
+
4
+ ADMIN_SECRET = os.environ.get("ADMIN_SECRET", "changeme")
5
+
6
+ def gen_key(tier: str) -> str:
7
+ prefix = "PRF" if tier == "free" else "PRP"
8
+ b = secrets.token_hex(10).upper()
9
+ return f"{prefix}-{b[:5]}-{b[5:10]}-{b[10:15]}-{b[15:20]}"
10
+
11
+ def hash_hwid(hwid: str) -> str:
12
+ return hmac.new(ADMIN_SECRET.encode(), hwid.encode(), hashlib.sha256).hexdigest()[:48]
13
+
14
+ def is_admin(token: str) -> bool:
15
+ return hmac.compare_digest(token, ADMIN_SECRET)
16
+
17
+ def free_expiry() -> datetime:
18
+ return datetime.utcnow() + timedelta(hours=24)
19
+
20
+ def is_expired(dt) -> bool:
21
+ return dt is not None and datetime.utcnow() > dt
@@ -0,0 +1,2 @@
1
+ """Pyrph — Python Obfuscation Engine"""
2
+ __version__ = "1.0.0"
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Pyrph — Python Obfuscation Engine
4
+ Entry point: python -m pyrph OR pyrph (after pip install)
5
+ """
6
+ import sys
7
+ import os
8
+ import ast
9
+ import time
10
+ from pathlib import Path
11
+
12
+
13
+ # ── ANSI colors ───────────────────────────────────────────────────────────
14
+ R="\033[0m"; B="\033[1m"; DIM="\033[2m"
15
+ CY="\033[36m"; GR="\033[32m"; YL="\033[33m"; RD="\033[31m"; PU="\033[35m"
16
+
17
+
18
+ def _c(s, *codes): return "".join(codes)+s+R
19
+
20
+
21
+ # ── Banner ────────────────────────────────────────────────────────────────
22
+ BANNER = f"""
23
+ {CY}{B} ██████╗ ██╗ ██╗██████╗ ██████╗ ██╗ ██╗
24
+ ██╔══██╗╚██╗ ██╔╝██╔══██╗██╔══██╗██║ ██║
25
+ ██████╔╝ ╚████╔╝ ██████╔╝██████╔╝███████║
26
+ ██╔═══╝ ╚██╔╝ ██╔══██╗██╔═══╝ ██╔══██║
27
+ ██║ ██║ ██║ ██║██║ ██║ ██║
28
+ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝{R}
29
+ {DIM} Python Obfuscation Engine · v1.0.0{R}
30
+ """
31
+
32
+
33
+ def _print_banner():
34
+ print(BANNER)
35
+
36
+
37
+ def _print_hwid_screen(hwid: str):
38
+ print(_c(" ┌─────────────────────────────────────────┐", DIM))
39
+ print(_c(" │ MACHINE IDENTIFIER │", DIM))
40
+ print(_c(" └─────────────────────────────────────────┘", DIM))
41
+ print()
42
+ print(f" {DIM}Your HWID:{R}")
43
+ print(f" {CY}{B}{hwid}{R}")
44
+ print()
45
+ print(f" {DIM}To use Pyrph, you need a key:{R}")
46
+ print(f" {GR}▸ Free key {R}{DIM}→ Visit: {R}{CY}https://pyrph.vercel.app/getkey{R}")
47
+ print(f" {YL}▸ Paid key {R}{DIM}→ Visit: {R}{CY}https://pyrph.vercel.app{R}")
48
+ print()
49
+ print(f" {DIM}Already have a key? Run:{R}")
50
+ print(f" {CY}pyrph --activate <your-key>{R}")
51
+ print()
52
+
53
+
54
+ def _print_tier_screen(tier: str, expires_at=None):
55
+ if tier == "paid":
56
+ icon = f"{GR}●{R}"
57
+ label = f"{GR}{B}PAID{R}"
58
+ info = f"{GR}Full access · Lifetime · Native + PolyVM + NestedVM{R}"
59
+ else:
60
+ icon = f"{YL}●{R}"
61
+ label = f"{YL}{B}FREE{R}"
62
+ exp = f" · Expires after this run" if expires_at else ""
63
+ info = f"{YL}Limited access · PolyVM only · 1 obfuscation{R}{exp}"
64
+
65
+ print(f" {icon} Status: {label}")
66
+ print(f" {DIM}{info}{R}")
67
+ print()
68
+
69
+
70
+ def _prompt_file() -> str:
71
+ print(f" {DIM}Enter the Python file to obfuscate:{R}")
72
+ try:
73
+ path = input(f" {CY}▸ File path: {R}").strip()
74
+ except (KeyboardInterrupt, EOFError):
75
+ print("\n Cancelled.")
76
+ sys.exit(0)
77
+ return path
78
+
79
+
80
+ def _run_obf(filepath: str, tier: str, key: str):
81
+ src_path = Path(filepath)
82
+
83
+ if not src_path.exists():
84
+ print(f"\n {RD}✗ File not found: {filepath}{R}")
85
+ sys.exit(1)
86
+
87
+ if not filepath.endswith(".py"):
88
+ print(f"\n {RD}✗ Only .py files supported.{R}")
89
+ sys.exit(1)
90
+
91
+ size_kb = src_path.stat().st_size / 1024
92
+ if tier == "free" and size_kb > 50:
93
+ print(f"\n {RD}✗ Free tier limited to 50KB. Your file: {size_kb:.1f}KB{R}")
94
+ print(f" {YL}Upgrade to paid for unlimited file size.{R}")
95
+ sys.exit(1)
96
+
97
+ # Determine profile and options based on tier
98
+ if tier == "paid":
99
+ profile = "balanced"
100
+ opts = dict(native=True, use_vm=True, vm_mode=False)
101
+ else:
102
+ # Free: PolyVM only, no Nested, no Native
103
+ profile = "balanced"
104
+ opts = dict(native=False, use_vm=True, vm_mode=False,
105
+ layer2=True, cff=True, mba=True)
106
+
107
+ print(f"\n {DIM}Reading {src_path.name} ({size_kb:.1f}KB)...{R}")
108
+ source = src_path.read_text(encoding="utf-8")
109
+
110
+ print(f" {DIM}Building pipeline [{profile}]...{R}\n")
111
+
112
+ try:
113
+ from pyrph.phases.unified import build_pipeline
114
+ p = build_pipeline(profile=profile, **opts)
115
+ t0 = time.time()
116
+ results = p.run(source)
117
+ elapsed = time.time() - t0
118
+ final = results[-1].code
119
+ except Exception as e:
120
+ print(f" {RD}✗ Obfuscation failed: {e}{R}")
121
+ sys.exit(1)
122
+
123
+ # Print pass results
124
+ for r in results:
125
+ icon = f"{GR}✓{R}" if r.success else f"{RD}✗{R}"
126
+ msg = f"{DIM}{r.message}{R}" if r.message else ""
127
+ print(f" {icon} {r.pass_name:<22} {msg}")
128
+
129
+ # Validate output
130
+ try:
131
+ ast.parse(final)
132
+ except SyntaxError as e:
133
+ print(f"\n {RD}✗ Output syntax error: {e}{R}")
134
+ sys.exit(1)
135
+
136
+ # Save output
137
+ out_path = src_path.with_stem(src_path.stem + "_obf")
138
+ out_path.write_text(final, encoding="utf-8")
139
+
140
+ in_l = len(source.splitlines())
141
+ out_l = len(final.splitlines())
142
+
143
+ print(f"\n {GR}✓ Done in {elapsed:.2f}s{R}")
144
+ print(f" {DIM}Lines: {in_l} → {out_l} · Size: {size_kb:.1f}KB → {len(final.encode())/1024:.1f}KB{R}")
145
+ print(f" {CY}Saved: {out_path}{R}\n")
146
+
147
+ # Mark free key as used (one-shot)
148
+ if tier == "free":
149
+ try:
150
+ from pyrph.key.client import mark_used
151
+ mark_used(key)
152
+ print(f" {YL}⚠ Free key expired. Get a new one at pyrph.vercel.app/getkey{R}\n")
153
+ except Exception:
154
+ pass
155
+
156
+
157
+ def main():
158
+ _print_banner()
159
+
160
+ args = sys.argv[1:]
161
+
162
+ # ── --activate <key> ──────────────────────────────────────────────────
163
+ if "--activate" in args or "-a" in args:
164
+ try:
165
+ idx = args.index("--activate") if "--activate" in args else args.index("-a")
166
+ key = args[idx + 1]
167
+ except IndexError:
168
+ print(f" {RD}Usage: pyrph --activate <key>{R}")
169
+ sys.exit(1)
170
+
171
+ print(f" {DIM}Activating key...{R}")
172
+ from pyrph.key.client import activate
173
+ result = activate(key)
174
+ if result.get("ok"):
175
+ tier = result.get("tier", "free")
176
+ print(f" {GR}✓ Activated! Tier: {tier.upper()}{R}")
177
+ if tier == "paid":
178
+ print(f" {GR}Full access unlocked. Run: pyrph{R}")
179
+ else:
180
+ print(f" {YL}Free access. Run: pyrph{R}")
181
+ else:
182
+ print(f" {RD}✗ {result.get('error') or result.get('detail', 'Activation failed')}{R}")
183
+ sys.exit(0)
184
+
185
+ # ── --getkey ──────────────────────────────────────────────────────────
186
+ if "--getkey" in args:
187
+ from pyrph.key.hwid import get_hwid
188
+ from pyrph.key.client import getkey_request
189
+ hwid = get_hwid()
190
+ print(f" {DIM}Requesting free key for HWID: {hwid[:16]}...{R}")
191
+ result = getkey_request(hwid)
192
+ if result.get("ok"):
193
+ key = result["key"]
194
+ print(f" {GR}✓ Key generated:{R}")
195
+ print(f" {CY}{B}{key}{R}")
196
+ print(f"\n {DIM}Activate it:{R} {CY}pyrph --activate {key}{R}")
197
+ else:
198
+ print(f" {RD}✗ {result.get('error','Failed')}{R}")
199
+ print(f" {DIM}Or visit: https://pyrph.vercel.app/getkey{R}")
200
+ sys.exit(0)
201
+
202
+ # ── --logout ──────────────────────────────────────────────────────────
203
+ if "--logout" in args:
204
+ from pyrph.key.client import delete_key
205
+ delete_key()
206
+ print(f" {GR}✓ Key removed.{R}")
207
+ sys.exit(0)
208
+
209
+ # ── Main flow ─────────────────────────────────────────────────────────
210
+ from pyrph.key.hwid import get_hwid
211
+ from pyrph.key.client import verify, load_key
212
+
213
+ hwid = get_hwid()
214
+ key = load_key() or os.environ.get("PYRPH_KEY")
215
+
216
+ if not key:
217
+ _print_hwid_screen(hwid)
218
+ sys.exit(0)
219
+
220
+ # Verify key
221
+ print(f" {DIM}Verifying key...{R}", end="", flush=True)
222
+ result = verify(key)
223
+ print(f"\r ", end="")
224
+
225
+ if not result.get("ok"):
226
+ err = result.get("error", "unknown")
227
+ if err == "offline":
228
+ print(f" {YL}⚠ Offline — using cached license.{R}")
229
+ elif err in ("expired", "Key expired."):
230
+ print(f" {RD}✗ Key expired.{R}")
231
+ from pyrph.key.client import delete_key
232
+ delete_key()
233
+ _print_hwid_screen(hwid)
234
+ sys.exit(1)
235
+ else:
236
+ print(f" {RD}✗ {err}{R}")
237
+ _print_hwid_screen(hwid)
238
+ sys.exit(1)
239
+ # Try cached result
240
+ result = {"ok": True, "tier": "free", "features": None}
241
+
242
+ tier = result.get("tier", "free")
243
+ expires_at = result.get("expires_at")
244
+
245
+ _print_tier_screen(tier, expires_at)
246
+
247
+ # Prompt for file
248
+ filepath = _prompt_file()
249
+ if not filepath:
250
+ print(f" {RD}No file entered.{R}")
251
+ sys.exit(1)
252
+
253
+ _run_obf(filepath, tier, key)
254
+
255
+
256
+ if __name__ == "__main__":
257
+ main()