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.
- pyrph-1.0.0/PKG-INFO +10 -0
- pyrph-1.0.0/api/__init__.py +0 -0
- pyrph-1.0.0/api/database.py +35 -0
- pyrph-1.0.0/api/main.py +234 -0
- pyrph-1.0.0/api/utils.py +21 -0
- pyrph-1.0.0/pyrph/__init__.py +2 -0
- pyrph-1.0.0/pyrph/__main__.py +257 -0
- pyrph-1.0.0/pyrph/bot.py +186 -0
- pyrph-1.0.0/pyrph/cli.py +170 -0
- pyrph-1.0.0/pyrph/core/__init__.py +4 -0
- pyrph-1.0.0/pyrph/core/base.py +28 -0
- pyrph-1.0.0/pyrph/core/pipeline.py +38 -0
- pyrph-1.0.0/pyrph/core/result.py +22 -0
- pyrph-1.0.0/pyrph/crypto/__init__.py +7 -0
- pyrph-1.0.0/pyrph/crypto/env_bind.py +191 -0
- pyrph-1.0.0/pyrph/crypto/keygen.py +88 -0
- pyrph-1.0.0/pyrph/key/__init__.py +3 -0
- pyrph-1.0.0/pyrph/key/client.py +148 -0
- pyrph-1.0.0/pyrph/key/hwid.py +35 -0
- pyrph-1.0.0/pyrph/native/__init__.py +4 -0
- pyrph-1.0.0/pyrph/native/builder.py +185 -0
- pyrph-1.0.0/pyrph/native/wb_aes.py +96 -0
- pyrph-1.0.0/pyrph/phases/__init__.py +2 -0
- pyrph-1.0.0/pyrph/phases/unified.py +252 -0
- pyrph-1.0.0/pyrph/transforms/__init__.py +23 -0
- pyrph-1.0.0/pyrph/transforms/anti_debug.py +241 -0
- pyrph-1.0.0/pyrph/transforms/anti_dump.py +165 -0
- pyrph-1.0.0/pyrph/transforms/cff.py +149 -0
- pyrph-1.0.0/pyrph/transforms/chaos.py +145 -0
- pyrph-1.0.0/pyrph/transforms/dead_code.py +103 -0
- pyrph-1.0.0/pyrph/transforms/expr_explode.py +161 -0
- pyrph-1.0.0/pyrph/transforms/import_obf.py +78 -0
- pyrph-1.0.0/pyrph/transforms/junk.py +83 -0
- pyrph-1.0.0/pyrph/transforms/mba.py +197 -0
- pyrph-1.0.0/pyrph/transforms/native_pack.py +225 -0
- pyrph-1.0.0/pyrph/transforms/number_enc.py +96 -0
- pyrph-1.0.0/pyrph/transforms/opaque.py +137 -0
- pyrph-1.0.0/pyrph/transforms/rename.py +204 -0
- pyrph-1.0.0/pyrph/transforms/self_mutate.py +124 -0
- pyrph-1.0.0/pyrph/transforms/string_vault.py +176 -0
- pyrph-1.0.0/pyrph/transforms/strip.py +53 -0
- pyrph-1.0.0/pyrph/vm/__init__.py +11 -0
- pyrph-1.0.0/pyrph/vm/compiler.py +607 -0
- pyrph-1.0.0/pyrph/vm/encryptor.py +209 -0
- pyrph-1.0.0/pyrph/vm/opcodes.py +168 -0
- pyrph-1.0.0/pyrph/vm/poly_vm_gen.py +342 -0
- pyrph-1.0.0/pyrph.egg-info/PKG-INFO +10 -0
- pyrph-1.0.0/pyrph.egg-info/SOURCES.txt +52 -0
- pyrph-1.0.0/pyrph.egg-info/dependency_links.txt +1 -0
- pyrph-1.0.0/pyrph.egg-info/entry_points.txt +2 -0
- pyrph-1.0.0/pyrph.egg-info/requires.txt +2 -0
- pyrph-1.0.0/pyrph.egg-info/top_level.txt +2 -0
- pyrph-1.0.0/setup.cfg +4 -0
- 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()
|
pyrph-1.0.0/api/main.py
ADDED
|
@@ -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"))
|
pyrph-1.0.0/api/utils.py
ADDED
|
@@ -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,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()
|