-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsetup.py
More file actions
405 lines (344 loc) · 12.9 KB
/
setup.py
File metadata and controls
405 lines (344 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
#!/usr/bin/env python3
"""
Setup script for 2FA Authenticator Tray
This script handles dependencies, protobuf generation, and file setup.
It does NOT run the tray applications - you'll do that manually.
"""
import os
import sys
import subprocess
import json
import shutil
import urllib.parse
import base64
import re
from pathlib import Path
# Project paths
PROJECT_ROOT = Path(__file__).parent
PROTO_FILE = PROJECT_ROOT / "auth_migration.proto"
PROTO_PY_FILE = PROJECT_ROOT / "auth_migration_pb2.py"
CSHARP_TRAY_DIR = PROJECT_ROOT / "csharp-tray" / "AuthenticatorTray"
CSHARP_ACCOUNTS_JSON = CSHARP_TRAY_DIR / "accounts.json"
CSHARP_ACCOUNTS_EXAMPLE = CSHARP_TRAY_DIR / "accounts.json.example"
IMG_PNG = PROJECT_ROOT / "img.png"
# Required Python packages (for QR decoding and protobuf)
REQUIRED_PACKAGES = [
"protobuf",
"pyzbar",
"Pillow",
]
def check_python_version():
"""Check if Python version is 3.6+"""
if sys.version_info < (3, 6):
print("❌ Python 3.6 or higher is required!")
sys.exit(1)
print(f"✅ Python {sys.version_info.major}.{sys.version_info.minor} detected")
def install_dependencies():
"""Install required Python packages"""
print("\n📦 Checking and installing dependencies...")
missing = []
for package in REQUIRED_PACKAGES:
try:
__import__(package.replace("-", "_"))
print(f" ✅ {package} is installed")
except ImportError:
missing.append(package)
print(f" ❌ {package} is missing")
if missing:
print(f"\n🔧 Installing missing packages: {', '.join(missing)}")
for package in missing:
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
print("✅ All dependencies installed!")
else:
print("✅ All dependencies are already installed!")
def generate_protobuf():
"""Generate auth_migration_pb2.py from auth_migration.proto"""
print("\n🔨 Checking protobuf file...")
if not PROTO_FILE.exists():
print(f"❌ {PROTO_FILE} not found!")
return False
# Check if generated file exists and is newer than proto file
if PROTO_PY_FILE.exists():
if PROTO_PY_FILE.stat().st_mtime > PROTO_FILE.stat().st_mtime:
print("✅ Protobuf file is up to date")
return True
print("🔧 Generating auth_migration_pb2.py from auth_migration.proto...")
try:
subprocess.check_call([
sys.executable, "-m", "grpc_tools.protoc",
f"--python_out={PROJECT_ROOT}",
f"--proto_path={PROJECT_ROOT}",
str(PROTO_FILE)
])
print("✅ Protobuf file generated successfully!")
return True
except subprocess.CalledProcessError:
print("⚠️ grpc_tools.protoc failed, trying alternative method...")
try:
# Alternative: use protoc directly if available
subprocess.check_call([
"protoc",
f"--python_out={PROJECT_ROOT}",
f"--proto_path={PROJECT_ROOT}",
str(PROTO_FILE)
])
print("✅ Protobuf file generated successfully!")
return True
except (subprocess.CalledProcessError, FileNotFoundError):
print("⚠️ Could not generate protobuf file automatically.")
print(" If auth_migration_pb2.py already exists, you can continue.")
print(" Otherwise, install protoc and run:")
print(f" protoc --python_out=. --proto_path=. {PROTO_FILE.name}")
return PROTO_PY_FILE.exists()
def parse_otpauth_url(url):
"""Parse an otpauth:// URL and extract account information"""
try:
parsed = urllib.parse.urlparse(url)
if parsed.scheme != "otpauth":
return None
# Extract label (issuer:account or just account)
label = urllib.parse.unquote(parsed.path.lstrip("/"))
# Parse query parameters
params = urllib.parse.parse_qs(parsed.query)
# Get secret (required)
secret = params.get("secret", [None])[0]
if not secret:
return None
# Get issuer and account name
issuer = params.get("issuer", [None])[0]
if ":" in label:
parts = label.split(":", 1)
if issuer:
account_name = parts[1] if len(parts) > 1 else parts[0]
else:
issuer = parts[0]
account_name = parts[1] if len(parts) > 1 else ""
else:
account_name = label
if not issuer:
issuer = ""
# Build display name
if issuer and account_name:
display_name = f"{issuer} ({account_name})"
elif issuer:
display_name = issuer
elif account_name:
display_name = account_name
else:
display_name = "Unknown"
# Get algorithm (default SHA1)
algorithm = params.get("algorithm", ["SHA1"])[0].upper()
if algorithm not in ["SHA1", "SHA256", "SHA512", "MD5"]:
algorithm = "SHA1"
# Get digits (default 6)
digits = int(params.get("digits", ["6"])[0])
if digits not in [6, 7, 8]:
digits = 6
return {
"name": display_name,
"secret": secret,
"digits": digits,
"algorithm": algorithm
}
except Exception as e:
print(f" ⚠️ Error parsing URL: {e}")
return None
def parse_migration_url(url):
"""Parse an otpauth-migration:// URL and extract all accounts"""
try:
# Parse the migration URL
parsed = urllib.parse.urlparse(url)
if parsed.scheme != "otpauth-migration":
return None
# Extract and decode the data parameter
params = urllib.parse.parse_qs(parsed.query)
if "data" not in params or not params["data"]:
return None
data = params["data"][0]
payload = base64.urlsafe_b64decode(data)
# Import the protobuf module
import auth_migration_pb2
# Parse the migration payload
migration = auth_migration_pb2.MigrationPayload()
migration.ParseFromString(payload)
accounts = []
for otp in migration.otp_parameters:
# Convert secret bytes to base32 string
secret_b32 = base64.b32encode(otp.secret).decode("utf-8")
# Build display name from issuer and name
issuer = otp.issuer if otp.issuer else ""
name = otp.name if otp.name else ""
if issuer and name:
display_name = f"{issuer} ({name})"
elif issuer:
display_name = issuer
elif name:
display_name = name
else:
display_name = "Unknown"
# Map algorithm enum to string
algorithm_map = {
1: "SHA1",
2: "SHA256",
3: "SHA512",
4: "MD5"
}
algorithm = algorithm_map.get(otp.algorithm, "SHA1")
# Get digits (default 6, validate that it's 6, 7, or 8)
digits = otp.digits if otp.digits in [6, 7, 8] else 6
accounts.append({
"name": display_name,
"secret": secret_b32,
"digits": digits,
"algorithm": algorithm
})
return accounts
except Exception as e:
print(f" ⚠️ Error parsing migration URL: {e}")
return None
def decode_qr_image(img_path):
"""Decode QR code from image and return otpauth URLs"""
try:
from pyzbar.pyzbar import decode
from PIL import Image
img = Image.open(img_path)
results = decode(img)
urls = []
for result in results:
data = result.data.decode("utf-8")
urls.append(data)
return urls
except Exception as e:
print(f" ❌ Error decoding QR code: {e}")
return []
def process_qr_code():
"""Automatically process img.png if it exists"""
if not IMG_PNG.exists():
return None
print(f"\n📷 Found {IMG_PNG.name}, decoding QR code...")
urls = decode_qr_image(IMG_PNG)
if not urls:
print(" ❌ No QR codes found in image!")
return None
print(f" ✅ Found {len(urls)} QR code(s)")
accounts = []
for url in urls:
if url.startswith("otpauth://"):
account = parse_otpauth_url(url)
if account:
accounts.append(account)
print(f" ✅ Parsed: {account['name']}")
elif url.startswith("otpauth-migration://"):
print(" 🔄 Processing migration URL...")
migration_accounts = parse_migration_url(url)
if migration_accounts:
accounts.extend(migration_accounts)
print(f" ✅ Extracted {len(migration_accounts)} account(s) from migration")
else:
print(" ❌ Failed to parse migration URL")
else:
print(f" ⚠️ Unknown URL format: {url[:50]}...")
if not accounts:
print(" ❌ No valid accounts found!")
return None
return accounts
def create_accounts_json(accounts):
"""Create accounts.json file for C# tray"""
if not accounts:
return False
# Create C# format (array)
csharp_accounts = {
"accounts": [
{
"name": acc["name"],
"secret": acc["secret"],
"digits": acc["digits"],
"algorithm": acc["algorithm"]
}
for acc in accounts
]
}
# Check if file exists and choose filename accordingly
csharp_file = CSHARP_ACCOUNTS_JSON
if CSHARP_ACCOUNTS_JSON.exists():
csharp_file = CSHARP_TRAY_DIR / "updated_accounts.json"
print(f"\n⚠️ {CSHARP_ACCOUNTS_JSON.name} already exists, creating {csharp_file.name} instead")
# Write C# accounts.json
CSHARP_TRAY_DIR.mkdir(parents=True, exist_ok=True)
with open(csharp_file, "w") as f:
json.dump(csharp_accounts, f, indent=2)
print(f"✅ Created {csharp_file}")
return True
def check_accounts_json():
"""Check if accounts.json file exists and is valid"""
print("\n📋 Checking accounts.json file...")
print(f"\n C# tray ({CSHARP_ACCOUNTS_JSON}):")
if CSHARP_ACCOUNTS_JSON.exists():
try:
with open(CSHARP_ACCOUNTS_JSON, "r") as f:
data = json.load(f)
print(" ✅ Exists and is valid JSON")
if isinstance(data, dict) and "accounts" in data:
count = len(data["accounts"])
else:
count = 0
print(f" 📊 Found {count} account(s)")
return True
except json.JSONDecodeError:
print(" ❌ Invalid JSON!")
return False
else:
if CSHARP_ACCOUNTS_EXAMPLE.exists():
print(" ⚠️ Not found, but example file exists")
else:
print(" ⚠️ Not found")
return False
def print_next_steps(accounts_exist, qr_processed=False):
"""Print next steps after setup"""
print("\n" + "=" * 60)
print("✅ Setup Complete!")
print("=" * 60)
if qr_processed:
print("\n📝 QR code processed and accounts.json file created!")
print(" You can now:")
else:
print("\n📝 Next Steps:")
print("\nTo build and run the C# tray application:")
if accounts_exist:
print(" ✅ accounts.json exists - ready to build!")
else:
print(" ⚠️ accounts.json not found")
print(" cd csharp-tray/AuthenticatorTray")
print(" dotnet build")
print(" dotnet run")
print("\n" + "=" * 60)
def main():
"""Main execution flow"""
print("=" * 60)
print("🔐 2FA Authenticator Tray - Setup Script")
print("=" * 60)
# Step 1: Check Python version
check_python_version()
# Step 2: Install dependencies
install_dependencies()
# Step 3: Generate protobuf file
generate_protobuf()
# Step 4: Check for img.png and process it automatically
qr_processed = False
if IMG_PNG.exists():
accounts = process_qr_code()
if accounts:
qr_processed = create_accounts_json(accounts)
# Step 5: Check accounts.json file
accounts_exist = check_accounts_json()
# Print next steps
print_next_steps(accounts_exist, qr_processed)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\n👋 Interrupted by user. Goodbye!")
except Exception as e:
print(f"\n❌ Error: {e}")
import traceback
traceback.print_exc()