From 096f0439f1deff92fd5c6015847b154b696a7c43 Mon Sep 17 00:00:00 2001 From: NehoayX Date: Sun, 9 Jun 2024 22:48:17 +0300 Subject: [PATCH 1/4] Add secure database, use TinyDB --- .gitignore | 4 + .vscode/settings.json | 4 +- main.py | 397 ++++++++++++++++++-------------- requirements.txt | 4 +- services/UI_database_service.py | 296 ++++++++++++++++++++++++ services/credentials_manager.py | 79 +++++++ services/upload.py | 1 + utilities/fs.py | 1 + 8 files changed, 609 insertions(+), 177 deletions(-) create mode 100644 services/UI_database_service.py create mode 100644 services/credentials_manager.py diff --git a/.gitignore b/.gitignore index daa872e..4cfbdda 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ config.json credentials.txt .ruff_cache .mypy_cache +brave.exe +125.1.66.115 +credentials.json +chromedriver.exe \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index ed99c2a..2327aca 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { - "editor.formatOnSave": true, + "editor.formatOnSave": false, "editor.formatOnPaste": true, "[python]": { - "editor.defaultFormatter": "charliermarsh.ruff" + "editor.defaultFormatter": "ms-python.black-formatter" } } diff --git a/main.py b/main.py index 9f14086..c852eba 100644 --- a/main.py +++ b/main.py @@ -9,210 +9,259 @@ from services.alive import keepalive from services.upload import upload_file -from services.extract import extract_credentials -from utilities.fs import ( - Config, - concrete_read_config, - read_config, - write_config, - write_default_config, - save_credentials, -) from utilities.web import ( - generate_mail, - type_name, - type_password, - initial_setup, - mail_login, - get_mail, + generate_mail, + type_name, + type_password, + initial_setup, + mail_login, + get_mail, ) from utilities.etc import ( - Credentials, - p_print, - clear_console, - Colours, - clear_tmp, - reinstall_tenacity, - check_for_updates, - delete_default, + Credentials, + p_print, + clear_console, + Colours, + clear_tmp, + reinstall_tenacity, + check_for_updates, + delete_default, ) +import getpass +from services.credentials_manager import CredentialsManager +import getpass +from tinydb import TinyDB, Query + +# Create an instance of the CredentialsManager class +cm = CredentialsManager() # Spooky import to check if the correct version of tenacity is installed. if sys.version_info.major == 3 and sys.version_info.minor <= 11: - try: - pass - except AttributeError: - reinstall_tenacity() + try: + pass + except AttributeError: + reinstall_tenacity() default_installs = [ - "C:/Program Files/Google/Chrome/Application/chrome.exe", - "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe", - "C:/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe", - "C:/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe", - "C:/Program Files/Microsoft/Edge/Application/msedge.exe", + "C:/Program Files/Google/Chrome/Application/chrome.exe", + "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe", + "C:/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe", + "C:/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe", + "C:/Program Files/Microsoft/Edge/Application/msedge.exe", ] args = [ - "--no-sandbox", - "--disable-setuid-sandbox", - "--disable-infobars", - "--window-position=0,0", - "--ignore-certificate-errors", - "--ignore-certificate-errors-spki-list", - '--user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0"', + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-infobars", + "--window-position=0,0", + "--ignore-certificate-errors", + "--ignore-certificate-errors-spki-list", + '--user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0"', ] parser = argparse.ArgumentParser() parser.add_argument( - "-ka", - "--keepalive", - required=False, - action="store_true", - help="Logs into the accounts to keep them alive.", + "-ka", + "--keepalive", + required=False, + action="store_true", + help="Logs into the accounts to keep them alive.", ) parser.add_argument( - "-e", - "--extract", - required=False, - action="store_true", - help="Extracts the credentials to a file.", + "-e", + "--extract", + required=False, + action="store_true", + help="Extracts the credentials to a file.", ) parser.add_argument( - "-v", - "--verbose", - required=False, - action="store_true", - help="Shows storage left while using keepalive function.", + "-v", + "--verbose", + required=False, + action="store_true", + help="Shows storage left while using keepalive function.", ) parser.add_argument( - "-f", "--file", required=False, help="Uploads a file to the account." + "-f", "--file", required=False, help="Uploads a file to the account." ) parser.add_argument( - "-p", - "--public", - required=False, - action="store_true", - help="Generates a public link to the uploaded file, use with -f", + "-p", + "--public", + required=False, + action="store_true", + help="Generates a public link to the uploaded file, use with -f", ) parser.add_argument( - "-l", - "--loop", - required=False, - help="Loops the program for a specified amount of times.", - type=int, + "-l", + "--loop", + required=False, + help="Loops the program for a specified amount of times.", + type=int, ) console_args = parser.parse_args() +config_db = TinyDB("config.json") + + +def setup() -> str: + """Sets up the configs so everything runs smoothly.""" + + if os.path.exists("config.json"): + s = config_db.all() + print(s) + account_format_exists = False + executable_path_exists = False + for item in s: + if "accountFormat" in item: + account_format_exists = True + if "executablePath" in item: + executable_path_exists = True + executable_path = item["executablePath"] + if not account_format_exists: + config_db.insert({"accountFormat": "{email}:{password}"}) + if not executable_path_exists: + config_db.insert({"executablePath": ""}) + else: + config_db.insert({"accountFormat": "{email}:{password}"}) + config_db.insert({"executablePath": ""}) + executable_path = "" + + # If no Chromium based browser is found, ask the user for the path to one. + if not executable_path_exists: + p_print( + "Failed to find a Chromium based browser. Please make sure you have one installed.", + Colours.FAIL, + ) + executable_path = input( + "Please enter the path to a Chromium based browser's executable: " + ) + if os.path.exists(executable_path): + p_print("Found executable!", Colours.OKGREEN) + config_db.update( + {"executablePath": executable_path}, Query().executablePath == "" + ) + else: + p_print("Failed to find executable!", Colours.FAIL) + sys.exit(1) + elif executable_path_exists: + p_print("Found executable!", Colours.OKGREEN) + + return executable_path + + +def loop_registrations(loop_count: int, executable_path: str): + """Registers accounts in a loop.""" + for _ in range(loop_count): + p_print(f"Loop {_ + 1}/{loop_count}", Colours.OKGREEN) + clear_tmp() + + credentials = asyncio.run(generate_mail()) + asyncio.run(register(credentials, executable_path)) + + +def get_matching_password(): + """ + Prompts the user to enter and confirm a password. + Returns the password if both inputs match. + Prompts again if the passwords don't match. + """ + while True: + password = getpass.getpass("Enter password to encrypt credentials: ") + password_2 = getpass.getpass("Re-enter password to confirm credentials: ") + + if password == password_2: + print("Passwords match.") + return password + else: + print("Passwords do not match. Please try again.") -def setup() -> Tuple[str, Config]: - """Sets up the configs so everything runs smoothly.""" - - executable_path = "" - config = read_config() - - if config is None: - write_default_config() - config = concrete_read_config() - else: - executable_path = config.executablePath - - # If no Chromium based browser is found, ask the user for the path to one. - if not executable_path: - p_print( - "Failed to find a Chromium based browser. Please make sure you have one installed.", - Colours.FAIL, - ) - executable_path = input( - "Please enter the path to a Chromium based browser's executable: " - ) - if os.path.exists(executable_path): - p_print("Found executable!", Colours.OKGREEN) - write_config("executablePath", executable_path, config) - else: - p_print("Failed to find executable!", Colours.FAIL) - sys.exit(1) - - return executable_path, config - - -def loop_registrations(loop_count: int, executable_path: str, config: Config): - """Registers accounts in a loop.""" - for _ in range(loop_count): - p_print(f"Loop {_ + 1}/{loop_count}", Colours.OKGREEN) - clear_tmp() - - credentials = asyncio.run(generate_mail()) - asyncio.run(register(credentials, executable_path, config)) - - -async def register(credentials: Credentials, executable_path: str, config: Config): - """Registers and verifies mega.nz account.""" - browser = await pyppeteer.launch( - { - "headless": True, - "ignoreHTTPSErrors": True, - "userDataDir": f"{os.getcwd()}/tmp", - "args": args, - "executablePath": executable_path, - "autoClose": False, - "ignoreDefaultArgs": ["--enable-automation", "--disable-extensions"], - } - ) - - context = await browser.createIncognitoBrowserContext() - page = await context.newPage() - - await type_name(page, credentials) - await type_password(page, credentials) - mail = await mail_login(credentials) - - await asyncio.sleep(1.5) - message = await get_mail(mail) - - await initial_setup(context, message, credentials) - await asyncio.sleep(0.5) - await browser.close() - - p_print("Verified account.", Colours.OKGREEN) - p_print( - f"Email: {credentials.email}\nPassword: {credentials.password}", - Colours.OKCYAN, - ) - - delete_default(credentials) - save_credentials(credentials, config.accountFormat) - - if console_args.file is not None: - file_size = os.path.getsize(console_args.file) - if os.path.exists(console_args.file) and 0 < file_size < 2e10: - if file_size >= 5e9: - p_print( - "File is larger than 5GB, mega.nz limits traffic to 5GB per IP.", - Colours.WARNING, - ) - upload_file(console_args.public, console_args.file, credentials) - else: - p_print("File not found.", Colours.FAIL) - if console_args.loop is None or console_args.loop <= 1: - sys.exit(0) +async def register(credentials: Credentials, executable_path: str): + """Registers and verifies mega.nz account.""" + browser = await pyppeteer.launch( + { + "headless": True, + "ignoreHTTPSErrors": True, + "userDataDir": f"{os.getcwd()}/tmp", + "args": args, + "executablePath": executable_path, + "autoClose": False, + "ignoreDefaultArgs": ["--enable-automation", "--disable-extensions"], + } + ) + + context = await browser.createIncognitoBrowserContext() + page = await context.newPage() + + await type_name(page, credentials) + await type_password(page, credentials) + mail = await mail_login(credentials) + + await asyncio.sleep(1.5) + message = await get_mail(mail) + + await initial_setup(context, message, credentials) + await asyncio.sleep(0.5) + await browser.close() + + p_print("Verified account.", Colours.OKGREEN) + p_print( + f"Email: {credentials.email}\nPassword: {credentials.password}", + Colours.OKCYAN, + ) + + # Store the credentials using the CredentialsManager class + password = get_matching_password() + + encrypted_credentials, salt = cm.encrypt_credentials( + password, + f"{credentials.email}:{credentials.emailPassword}:{credentials.id}:{credentials.password}", + ) + encrypted_credentials_hex = encrypted_credentials.hex() + salt_hex = salt.hex() + + cm.store_credentials( + password, credentials.email, encrypted_credentials_hex, salt_hex + ) + + delete_default(credentials) + + if console_args.file is not None: + file_size = os.path.getsize(console_args.file) + if os.path.exists(console_args.file) and 0 < file_size < 2e10: + if file_size >= 5e9: + p_print( + "File is larger than 5GB, mega.nz limits traffic to 5GB per IP.", + Colours.WARNING, + ) + successful = upload_file(console_args.public, console_args.file, credentials) + if successful is not None: + successful = str(successful) + cm.update_account_mega(str(credentials.email),successful) + + else: + p_print("File not found.", Colours.FAIL) + if console_args.loop is None or console_args.loop <= 1: + sys.exit(0) if __name__ == "__main__": - clear_console() - check_for_updates() - - executable_path, config = setup() - if not executable_path: - p_print("Failed while setting up!", Colours.FAIL) - sys.exit(1) - - if console_args.extract: - extract_credentials(config.accountFormat) - elif console_args.keepalive: - keepalive(console_args.verbose) - elif console_args.loop is not None and console_args.loop > 1: - loop_registrations(console_args.loop, executable_path, config) - else: - clear_tmp() - credentials = asyncio.run(generate_mail()) - asyncio.run(register(credentials, executable_path, config)) + clear_console() + check_for_updates() + + executable_path = setup() + if not executable_path: + p_print("Failed while setting up!", Colours.FAIL) + sys.exit(1) + + if console_args.extract: + ... + # extract_credentials(config.accountFormat) + elif console_args.keepalive: + keepalive(console_args.verbose) + elif console_args.loop is not None and console_args.loop > 1: + loop_registrations(console_args.loop, executable_path) + else: + clear_tmp() + credentials = asyncio.run(generate_mail()) + asyncio.run(register(credentials, executable_path)) diff --git a/requirements.txt b/requirements.txt index 2c8dd4d..49df63f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,6 @@ Faker>=13.11.1 pymailtm>=1.0.2 pyppeteer>=1.0.2 psutil>=5.9.3 -mega.py>=1.0.8 \ No newline at end of file +mega.py>=1.0.8 +cryptography>=42.0.8 +tinydb>=4.8.0 \ No newline at end of file diff --git a/services/UI_database_service.py b/services/UI_database_service.py new file mode 100644 index 0000000..3905cf3 --- /dev/null +++ b/services/UI_database_service.py @@ -0,0 +1,296 @@ +# Modify the code below to create a nice and good looking dark mode, that is light on the eyes and looks great, use these colors: Black #24262C, Pink #C9ADA7, Rose quartz #9A8C98 + + +import tkinter as tk +from tkinter import ttk +from credentials_manager import CredentialsManager + + +class CredentialsViewer: + def __init__(self, master): + self.master = master + master.title("Credentials Viewer") + + self.toast_canvas = None # Canvas for toast messages + self.toast_message_id = None # Text id for the toast message + + self.dark_mode = tk.BooleanVar() + self.dark_mode.set(True) # Start in dark mode + + self.style = ttk.Style() + self.configure_dark_mode() + ttk.Style().configure( + "green/black.TLabel", + foreground="#9A8C98", + background="#111111", + font=("Helvetica", 10, "bold"), + ) + ttk.Style().configure( + "green/black.TButton", foreground="#9A8C98", background="#111111" + ) + ttk.Style().configure( + "green/black.TEntry", foreground="#9A8C98", background="#111111" + ) + ttk.Style().configure( + "green/black.TCheckbutton", foreground="#9A8C98", background="#111111" + ) + ttk.Style().configure( + "green/black.THeading", foreground="#9A8C98", background="#111111" + ) + + master.configure(background="#24262C") + + self.dark_mode_checkbox = ttk.Checkbutton( + master, + style="green/black.TCheckbutton", + text="Dark Mode", + variable=self.dark_mode, + command=self.toggle_dark_mode, + ) + self.dark_mode_checkbox.grid(row=0, column=0, padx=5, pady=5) + + self.password_label = ttk.Label(master, text="Password:") + self.password_label.grid(row=1, column=0, padx=5, pady=5) + + self.password_entry = ttk.Entry(master, show="*", style="green/black.TEntry") + self.password_entry.grid(row=1, column=1, padx=5, pady=5) + + self.decrypt_button = ttk.Button( + master, + text="Decrypt", + command=self.decrypt_credentials, + style="green/black.TButton", + ) + self.decrypt_button.grid(row=1, column=2, padx=5, pady=5) + + self.credentials_tree = ttk.Treeview( + master, + style="green/black.TLabel", + columns=( + "Email", + "Email Password", + "Account ID", + "Account Password", + "Mega LINKs", + ), + show="headings", + ) + self.credentials_tree.heading("Email", text="Email") + self.credentials_tree.heading("Email Password", text="Email Password") + self.credentials_tree.heading("Account ID", text="Account ID") + self.credentials_tree.heading("Account Password", text="Account Password") + self.credentials_tree.heading("#5", text="MEGA DL Links") + self.credentials_tree.grid(row=2, column=0, columnspan=3, padx=5, pady=5) + + self.status_label = ttk.Label(master, text="") + self.status_label.grid(row=3, column=0, columnspan=3, padx=5, pady=5) + + self.cm = CredentialsManager() # Replace with your actual CredentialsManager + + # Apply dark mode styling initially + self.configure_dark_mode() + + # Context Menu + self.context_menu = tk.Menu(master, tearoff=0) + self.context_menu.add_command(label="Copy", command=self.copy_selected_value) + + # Keep track of the right-clicked event + self.right_click_event = None + + # Bind right-click to show the context menu + self.credentials_tree.bind("", self.show_context_menu) + + def configure_dark_mode(self): + if self.dark_mode.get(): + self.master.configure(background="#24262C") + self.style.configure("TLabel", background="#24262C", foreground="#C9ADA7") + self.style.configure( + "TEntry", + background="#9A8C98", + foreground="#24262C", + insertcolor="#C9ADA7", + fieldbackground="#9A8C98", + ) + self.style.configure( + "TButton", + background="#9A8C98", + foreground="#24262C", + relief="flat", + borderwidth=0, + ) + self.style.configure( + "Treeview", + background="#24262C", + foreground="#C9ADA7", + fieldbackground="#33353F", + selectbackground="#9A8C98", + selectforeground="#24262C", + rowheight=30, + font=("Helvetica", 20, "bold"), + ) + self.style.configure( + "Treeview.Heading", + background="#33353F", + foreground="#C9ADA7", + relief="flat", + font=("Helvetica", 13, "bold"), + ) + + self.style.map( + "Treeview", + background=[("selected", "#9A8C98")], + foreground=[("selected", "#24262C")], + ) + else: + self.master.configure(background="white") + self.style.configure("TLabel", background="white", foreground="black") + self.style.configure("TEntry", background="white", foreground="black") + self.style.configure( + "TButton", background="SystemButtonFace", foreground="black" + ) + self.style.configure( + "Treeview", + background="white", + foreground="black", + fieldbackground="white", + ) + self.style.configure( + "Treeview.Heading", background="SystemButtonFace", foreground="black" + ) + + def toggle_dark_mode(self): + self.configure_dark_mode() + + def decrypt_credentials(self): + password = self.password_entry.get() + credentials_list = self.cm.get_credentials(password) + + if credentials_list: + self.credentials_tree.delete(*self.credentials_tree.get_children()) + for credentials in credentials_list: + email = credentials[0] if credentials else "" + email_password = credentials[1] if len(credentials) > 1 else "" + account_id = credentials[2] if len(credentials) > 2 else "" + account_password = credentials[3] if len(credentials) > 3 else "" + temp_links = self.cm.get_mega_links(email) + mega_links = temp_links if temp_links else "" + self.credentials_tree.insert( + "", + "end", + values=(email, email_password, account_id, account_password,mega_links), + ) + self.status_label.config(text="Credentials decrypted successfully.") + else: + self.status_label.config( + text="Failed to decrypt credentials. Please check the password and try again." + ) + + def show_context_menu(self, event): + self.right_click_event = event + try: + self.credentials_tree.selection_set( + self.credentials_tree.identify_row(event.y) + ) + self.context_menu.post(event.x_root, event.y_root) + finally: + self.context_menu.grab_release() + + def copy_selected_value(self): + if not self.right_click_event: + return + + region = self.credentials_tree.identify( + "region", self.right_click_event.x, self.right_click_event.y + ) + if region != "cell": + return + + column = self.credentials_tree.identify_column(self.right_click_event.x) + item = self.credentials_tree.identify_row(self.right_click_event.y) + if not column or not item: + return + + column_id = ( + int(column.replace("#", "")) - 1 + ) # Convert column index to zero-based + selected_value = self.credentials_tree.item(item)["values"][column_id] + + if selected_value: + self.master.clipboard_clear() + self.master.clipboard_append(selected_value) + self.master.update() # Keeps the clipboard data persistent + self.show_toast("Text copied!", "#28a745") + + def show_toast(self, message, color): + """ + Displays a toast message at a location that won't cover other widgets. + The message is rounded and styled. + """ + # Destroy any existing toast + if self.toast_canvas: + self.toast_canvas.destroy() + + # Avoid covering other widgets by positioning at the bottom-right corner + x = self.master.winfo_width() - 250 + y = self.master.winfo_height() - 80 + + # Create a new canvas for the toast + width, height = 200, 40 + radius = 20 + self.toast_canvas = tk.Canvas( + self.master, width=width, height=height, bg="#111111", highlightthickness=0 + ) + self.toast_canvas.place(x=x, y=y) + + # Draw a rounded rectangle using arcs and rectangles + self.toast_canvas.create_arc( + (0, 0, 2 * radius, 2 * radius), + start=90, + extent=90, + fill=color, + outline=color, + ) + self.toast_canvas.create_arc( + (width - 2 * radius, 0, width, 2 * radius), + start=0, + extent=90, + fill=color, + outline=color, + ) + self.toast_canvas.create_arc( + (0, height - 2 * radius, 2 * radius, height), + start=180, + extent=90, + fill=color, + outline=color, + ) + self.toast_canvas.create_arc( + (width - 2 * radius, height - 2 * radius, width, height), + start=270, + extent=90, + fill=color, + outline=color, + ) + self.toast_canvas.create_rectangle( + radius, 0, width - radius, height, fill=color, outline=color + ) + self.toast_canvas.create_rectangle( + 0, radius, width, height - radius, fill=color, outline=color + ) + + # Add the text to the toast + self.toast_message_id = self.toast_canvas.create_text( + width / 2, + height / 2, + text=message, + fill="white", + font=("Helvetica", 12, "bold"), + ) + + # Auto-destroy after 2500 ms + self.master.after(2500, self.toast_canvas.destroy) + + +root = tk.Tk() +viewer = CredentialsViewer(root) +root.mainloop() diff --git a/services/credentials_manager.py b/services/credentials_manager.py new file mode 100644 index 0000000..bb898bf --- /dev/null +++ b/services/credentials_manager.py @@ -0,0 +1,79 @@ +import os +import base64 +from cryptography.fernet import Fernet, InvalidToken +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +import tinydb +from tinydb import Query + + +class CredentialsManager: + def __init__(self): + self.db = tinydb.TinyDB("credentials.json") + + def encrypt_credentials(self, password, credentials): + password = password.encode() # convert to bytes + salt = os.urandom(16) # generate a random salt + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), length=32, salt=salt, iterations=100000 + ) + key = base64.urlsafe_b64encode(kdf.derive(password)) + f = Fernet(key) + encrypted_credentials = f.encrypt(credentials.encode()) + return encrypted_credentials, salt + + def decrypt_credentials(self, password, encrypted_credentials, salt): + try: + password = password.encode() # convert to bytes + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), length=32, salt=salt, iterations=100000 + ) + key = base64.urlsafe_b64encode(kdf.derive(password)) + f = Fernet(key) + decrypted_credentials = f.decrypt(encrypted_credentials).decode() + return decrypted_credentials + except (ValueError, InvalidToken): + return None + + def store_credentials(self, password, email, encrypted_credentials_hex, salt_hex): + self.db.insert( + {"email": email, "credentials": encrypted_credentials_hex, "salt": salt_hex} + ) + + def get_credentials(self, password): + query = Query() + results = self.db.search(query.email.exists()) + decrypted_credentials = [] + for result in results: + try: + encrypted_credentials_hex = result["credentials"] + salt_hex = result["salt"] + encrypted_credentials = bytes.fromhex(encrypted_credentials_hex) + salt = bytes.fromhex(salt_hex) + decrypted_credential = self.decrypt_credentials( + password, encrypted_credentials, salt + ) + if decrypted_credential: + decrypted_credentials.append(decrypted_credential.split(":")) + except (ValueError, IndexError): + continue + return decrypted_credentials + + def update_account_mega(self,email,MegaLinkToAdd): + Emails = Query() + # self.db.search(Emails.email == f"{email}") + self.db.update({"megaLinks": [str(MegaLinkToAdd)]}, Emails.email == f"{email}") + return True + + + def get_mega_links(self, email): + Emails = Query() + results = self.db.search(Emails.email == f"{email}") + if len(results) == 0: + return ["No Links found"] + else: + result = results[0] + if "megaLinks" in result: + return result["megaLinks"] + else: + return ["No megaLinks found"] diff --git a/services/upload.py b/services/upload.py index 576a9da..8c89481 100644 --- a/services/upload.py +++ b/services/upload.py @@ -17,3 +17,4 @@ def upload_file(public: bool, file: str, credentials: Credentials): if public: link = mega.get_upload_link(uploaded_file) p_print(f"Shareable link: {link}", Colours.OKGREEN) + return str(link) diff --git a/utilities/fs.py b/utilities/fs.py index 9a45105..8f3ff62 100644 --- a/utilities/fs.py +++ b/utilities/fs.py @@ -7,6 +7,7 @@ from utilities.etc import p_print from utilities.types import Colours, Credentials, Config +from typing import Union CONFIG_FILE = "config.json" From 8ec0abc1897632de27bd836410354008cb64dd3e Mon Sep 17 00:00:00 2001 From: Nehoray_Developer <66194934+NEDev2@users.noreply.github.com> Date: Sun, 9 Jun 2024 22:49:55 +0300 Subject: [PATCH 2/4] Update .gitignore --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 4cfbdda..a332bb6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,5 @@ credentials.txt .ruff_cache .mypy_cache brave.exe -125.1.66.115 credentials.json -chromedriver.exe \ No newline at end of file +chromedriver.exe From c223fe9c5f87cf272513b5a90ccec42b68f13ef0 Mon Sep 17 00:00:00 2001 From: Nehoray_Developer <66194934+NEDev2@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:54:54 +0300 Subject: [PATCH 3/4] Update settings.json As requested modified :) --- .vscode/settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2327aca..ed99c2a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { - "editor.formatOnSave": false, + "editor.formatOnSave": true, "editor.formatOnPaste": true, "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter" + "editor.defaultFormatter": "charliermarsh.ruff" } } From 1812af1fec11bec3f990555fc4a2b14dcdc13e56 Mon Sep 17 00:00:00 2001 From: qtchaos <72168435+qtchaos@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:03:09 +0300 Subject: [PATCH 4/4] chore: fix formatting, organize files --- main.py | 406 +++++++++++++++----------------- services/UI_database_service.py | 296 ----------------------- services/credentials_manager.py | 79 ------- services/gui.py | 382 ++++++++++++++++++++++++++++++ utilities/etc.py | 18 ++ utilities/fs.py | 1 - 6 files changed, 595 insertions(+), 587 deletions(-) delete mode 100644 services/UI_database_service.py delete mode 100644 services/credentials_manager.py create mode 100644 services/gui.py diff --git a/main.py b/main.py index c852eba..630755c 100644 --- a/main.py +++ b/main.py @@ -4,32 +4,31 @@ import argparse import os import sys -from typing import Tuple import pyppeteer from services.alive import keepalive from services.upload import upload_file from utilities.web import ( - generate_mail, - type_name, - type_password, - initial_setup, - mail_login, - get_mail, + generate_mail, + type_name, + type_password, + initial_setup, + mail_login, + get_mail, ) from utilities.etc import ( - Credentials, - p_print, - clear_console, - Colours, - clear_tmp, - reinstall_tenacity, - check_for_updates, - delete_default, + Credentials, + p_print, + clear_console, + Colours, + clear_tmp, + reinstall_tenacity, + check_for_updates, + delete_default, + get_matching_password, ) -import getpass -from services.credentials_manager import CredentialsManager -import getpass + +from services.gui import CredentialsManager from tinydb import TinyDB, Query # Create an instance of the CredentialsManager class @@ -37,66 +36,66 @@ # Spooky import to check if the correct version of tenacity is installed. if sys.version_info.major == 3 and sys.version_info.minor <= 11: - try: - pass - except AttributeError: - reinstall_tenacity() + try: + pass + except AttributeError: + reinstall_tenacity() default_installs = [ - "C:/Program Files/Google/Chrome/Application/chrome.exe", - "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe", - "C:/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe", - "C:/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe", - "C:/Program Files/Microsoft/Edge/Application/msedge.exe", + "C:/Program Files/Google/Chrome/Application/chrome.exe", + "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe", + "C:/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe", + "C:/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe", + "C:/Program Files/Microsoft/Edge/Application/msedge.exe", ] args = [ - "--no-sandbox", - "--disable-setuid-sandbox", - "--disable-infobars", - "--window-position=0,0", - "--ignore-certificate-errors", - "--ignore-certificate-errors-spki-list", - '--user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0"', + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-infobars", + "--window-position=0,0", + "--ignore-certificate-errors", + "--ignore-certificate-errors-spki-list", + '--user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0"', ] parser = argparse.ArgumentParser() parser.add_argument( - "-ka", - "--keepalive", - required=False, - action="store_true", - help="Logs into the accounts to keep them alive.", + "-ka", + "--keepalive", + required=False, + action="store_true", + help="Logs into the accounts to keep them alive.", ) parser.add_argument( - "-e", - "--extract", - required=False, - action="store_true", - help="Extracts the credentials to a file.", + "-e", + "--extract", + required=False, + action="store_true", + help="Extracts the credentials to a file.", ) parser.add_argument( - "-v", - "--verbose", - required=False, - action="store_true", - help="Shows storage left while using keepalive function.", + "-v", + "--verbose", + required=False, + action="store_true", + help="Shows storage left while using keepalive function.", ) parser.add_argument( - "-f", "--file", required=False, help="Uploads a file to the account." + "-f", "--file", required=False, help="Uploads a file to the account." ) parser.add_argument( - "-p", - "--public", - required=False, - action="store_true", - help="Generates a public link to the uploaded file, use with -f", + "-p", + "--public", + required=False, + action="store_true", + help="Generates a public link to the uploaded file, use with -f", ) parser.add_argument( - "-l", - "--loop", - required=False, - help="Loops the program for a specified amount of times.", - type=int, + "-l", + "--loop", + required=False, + help="Loops the program for a specified amount of times.", + type=int, ) console_args = parser.parse_args() @@ -104,164 +103,149 @@ def setup() -> str: - """Sets up the configs so everything runs smoothly.""" - - if os.path.exists("config.json"): - s = config_db.all() - print(s) - account_format_exists = False - executable_path_exists = False - for item in s: - if "accountFormat" in item: - account_format_exists = True - if "executablePath" in item: - executable_path_exists = True - executable_path = item["executablePath"] - if not account_format_exists: - config_db.insert({"accountFormat": "{email}:{password}"}) - if not executable_path_exists: - config_db.insert({"executablePath": ""}) - else: - config_db.insert({"accountFormat": "{email}:{password}"}) - config_db.insert({"executablePath": ""}) - executable_path = "" - - # If no Chromium based browser is found, ask the user for the path to one. - if not executable_path_exists: - p_print( - "Failed to find a Chromium based browser. Please make sure you have one installed.", - Colours.FAIL, - ) - executable_path = input( - "Please enter the path to a Chromium based browser's executable: " - ) - if os.path.exists(executable_path): - p_print("Found executable!", Colours.OKGREEN) - config_db.update( - {"executablePath": executable_path}, Query().executablePath == "" - ) - else: - p_print("Failed to find executable!", Colours.FAIL) - sys.exit(1) - elif executable_path_exists: - p_print("Found executable!", Colours.OKGREEN) - - return executable_path + """Sets up the configs so everything runs smoothly.""" + + if os.path.exists("config.json"): + s = config_db.all() + print(s) + account_format_exists = False + executable_path_exists = False + for item in s: + if "accountFormat" in item: + account_format_exists = True + if "executablePath" in item: + executable_path_exists = True + executable_path = item["executablePath"] + if not account_format_exists: + config_db.insert({"accountFormat": "{email}:{password}"}) + if not executable_path_exists: + config_db.insert({"executablePath": ""}) + else: + config_db.insert({"accountFormat": "{email}:{password}"}) + config_db.insert({"executablePath": ""}) + executable_path = "" + + # If no Chromium based browser is found, ask the user for the path to one. + if not executable_path_exists: + p_print( + "Failed to find a Chromium based browser. Please make sure you have one installed.", + Colours.FAIL, + ) + executable_path = input( + "Please enter the path to a Chromium based browser's executable: " + ) + if os.path.exists(executable_path): + p_print("Found executable!", Colours.OKGREEN) + config_db.update( + {"executablePath": executable_path}, Query().executablePath == "" + ) + else: + p_print("Failed to find executable!", Colours.FAIL) + sys.exit(1) + elif executable_path_exists: + p_print("Found executable!", Colours.OKGREEN) + + return executable_path def loop_registrations(loop_count: int, executable_path: str): - """Registers accounts in a loop.""" - for _ in range(loop_count): - p_print(f"Loop {_ + 1}/{loop_count}", Colours.OKGREEN) - clear_tmp() - - credentials = asyncio.run(generate_mail()) - asyncio.run(register(credentials, executable_path)) - - -def get_matching_password(): - """ - Prompts the user to enter and confirm a password. - Returns the password if both inputs match. - Prompts again if the passwords don't match. - """ - while True: - password = getpass.getpass("Enter password to encrypt credentials: ") - password_2 = getpass.getpass("Re-enter password to confirm credentials: ") + """Registers accounts in a loop.""" + for _ in range(loop_count): + p_print(f"Loop {_ + 1}/{loop_count}", Colours.OKGREEN) + clear_tmp() - if password == password_2: - print("Passwords match.") - return password - else: - print("Passwords do not match. Please try again.") + credentials = asyncio.run(generate_mail()) + asyncio.run(register(credentials, executable_path)) async def register(credentials: Credentials, executable_path: str): - """Registers and verifies mega.nz account.""" - browser = await pyppeteer.launch( - { - "headless": True, - "ignoreHTTPSErrors": True, - "userDataDir": f"{os.getcwd()}/tmp", - "args": args, - "executablePath": executable_path, - "autoClose": False, - "ignoreDefaultArgs": ["--enable-automation", "--disable-extensions"], - } - ) - - context = await browser.createIncognitoBrowserContext() - page = await context.newPage() - - await type_name(page, credentials) - await type_password(page, credentials) - mail = await mail_login(credentials) - - await asyncio.sleep(1.5) - message = await get_mail(mail) - - await initial_setup(context, message, credentials) - await asyncio.sleep(0.5) - await browser.close() - - p_print("Verified account.", Colours.OKGREEN) - p_print( - f"Email: {credentials.email}\nPassword: {credentials.password}", - Colours.OKCYAN, - ) - - # Store the credentials using the CredentialsManager class - password = get_matching_password() - - encrypted_credentials, salt = cm.encrypt_credentials( - password, - f"{credentials.email}:{credentials.emailPassword}:{credentials.id}:{credentials.password}", - ) - encrypted_credentials_hex = encrypted_credentials.hex() - salt_hex = salt.hex() - - cm.store_credentials( - password, credentials.email, encrypted_credentials_hex, salt_hex - ) - - delete_default(credentials) - - if console_args.file is not None: - file_size = os.path.getsize(console_args.file) - if os.path.exists(console_args.file) and 0 < file_size < 2e10: - if file_size >= 5e9: - p_print( - "File is larger than 5GB, mega.nz limits traffic to 5GB per IP.", - Colours.WARNING, - ) - successful = upload_file(console_args.public, console_args.file, credentials) - if successful is not None: - successful = str(successful) - cm.update_account_mega(str(credentials.email),successful) - - else: - p_print("File not found.", Colours.FAIL) - if console_args.loop is None or console_args.loop <= 1: - sys.exit(0) + """Registers and verifies mega.nz account.""" + browser = await pyppeteer.launch( + { + "headless": True, + "ignoreHTTPSErrors": True, + "userDataDir": f"{os.getcwd()}/tmp", + "args": args, + "executablePath": executable_path, + "autoClose": False, + "ignoreDefaultArgs": ["--enable-automation", "--disable-extensions"], + } + ) + + context = await browser.createIncognitoBrowserContext() + page = await context.newPage() + + await type_name(page, credentials) + await type_password(page, credentials) + mail = await mail_login(credentials) + + await asyncio.sleep(1.5) + message = await get_mail(mail) + + await initial_setup(context, message, credentials) + await asyncio.sleep(0.5) + await browser.close() + + p_print("Verified account.", Colours.OKGREEN) + p_print( + f"Email: {credentials.email}\nPassword: {credentials.password}", + Colours.OKCYAN, + ) + + # Store the credentials using the CredentialsManager class + password = get_matching_password() + + encrypted_credentials, salt = cm.encrypt_credentials( + password, + f"{credentials.email}:{credentials.emailPassword}:{credentials.id}:{credentials.password}", + ) + encrypted_credentials_hex = encrypted_credentials.hex() + salt_hex = salt.hex() + + cm.store_credentials( + password, credentials.email, encrypted_credentials_hex, salt_hex + ) + + delete_default(credentials) + + if console_args.file is not None: + file_size = os.path.getsize(console_args.file) + if os.path.exists(console_args.file) and 0 < file_size < 2e10: + if file_size >= 5e9: + p_print( + "File is larger than 5GB, mega.nz limits traffic to 5GB per IP.", + Colours.WARNING, + ) + successful = upload_file( + console_args.public, console_args.file, credentials + ) + if successful is not None: + successful = str(successful) + cm.update_account_mega(str(credentials.email), successful) + + else: + p_print("File not found.", Colours.FAIL) + if console_args.loop is None or console_args.loop <= 1: + sys.exit(0) if __name__ == "__main__": - clear_console() - check_for_updates() - - executable_path = setup() - if not executable_path: - p_print("Failed while setting up!", Colours.FAIL) - sys.exit(1) - - if console_args.extract: - ... - # extract_credentials(config.accountFormat) - elif console_args.keepalive: - keepalive(console_args.verbose) - elif console_args.loop is not None and console_args.loop > 1: - loop_registrations(console_args.loop, executable_path) - else: - clear_tmp() - credentials = asyncio.run(generate_mail()) - asyncio.run(register(credentials, executable_path)) + clear_console() + check_for_updates() + + executable_path = setup() + if not executable_path: + p_print("Failed while setting up!", Colours.FAIL) + sys.exit(1) + + if console_args.extract: + ... + # extract_credentials(config.accountFormat) + elif console_args.keepalive: + keepalive(console_args.verbose) + elif console_args.loop is not None and console_args.loop > 1: + loop_registrations(console_args.loop, executable_path) + else: + clear_tmp() + credentials = asyncio.run(generate_mail()) + asyncio.run(register(credentials, executable_path)) diff --git a/services/UI_database_service.py b/services/UI_database_service.py deleted file mode 100644 index 3905cf3..0000000 --- a/services/UI_database_service.py +++ /dev/null @@ -1,296 +0,0 @@ -# Modify the code below to create a nice and good looking dark mode, that is light on the eyes and looks great, use these colors: Black #24262C, Pink #C9ADA7, Rose quartz #9A8C98 - - -import tkinter as tk -from tkinter import ttk -from credentials_manager import CredentialsManager - - -class CredentialsViewer: - def __init__(self, master): - self.master = master - master.title("Credentials Viewer") - - self.toast_canvas = None # Canvas for toast messages - self.toast_message_id = None # Text id for the toast message - - self.dark_mode = tk.BooleanVar() - self.dark_mode.set(True) # Start in dark mode - - self.style = ttk.Style() - self.configure_dark_mode() - ttk.Style().configure( - "green/black.TLabel", - foreground="#9A8C98", - background="#111111", - font=("Helvetica", 10, "bold"), - ) - ttk.Style().configure( - "green/black.TButton", foreground="#9A8C98", background="#111111" - ) - ttk.Style().configure( - "green/black.TEntry", foreground="#9A8C98", background="#111111" - ) - ttk.Style().configure( - "green/black.TCheckbutton", foreground="#9A8C98", background="#111111" - ) - ttk.Style().configure( - "green/black.THeading", foreground="#9A8C98", background="#111111" - ) - - master.configure(background="#24262C") - - self.dark_mode_checkbox = ttk.Checkbutton( - master, - style="green/black.TCheckbutton", - text="Dark Mode", - variable=self.dark_mode, - command=self.toggle_dark_mode, - ) - self.dark_mode_checkbox.grid(row=0, column=0, padx=5, pady=5) - - self.password_label = ttk.Label(master, text="Password:") - self.password_label.grid(row=1, column=0, padx=5, pady=5) - - self.password_entry = ttk.Entry(master, show="*", style="green/black.TEntry") - self.password_entry.grid(row=1, column=1, padx=5, pady=5) - - self.decrypt_button = ttk.Button( - master, - text="Decrypt", - command=self.decrypt_credentials, - style="green/black.TButton", - ) - self.decrypt_button.grid(row=1, column=2, padx=5, pady=5) - - self.credentials_tree = ttk.Treeview( - master, - style="green/black.TLabel", - columns=( - "Email", - "Email Password", - "Account ID", - "Account Password", - "Mega LINKs", - ), - show="headings", - ) - self.credentials_tree.heading("Email", text="Email") - self.credentials_tree.heading("Email Password", text="Email Password") - self.credentials_tree.heading("Account ID", text="Account ID") - self.credentials_tree.heading("Account Password", text="Account Password") - self.credentials_tree.heading("#5", text="MEGA DL Links") - self.credentials_tree.grid(row=2, column=0, columnspan=3, padx=5, pady=5) - - self.status_label = ttk.Label(master, text="") - self.status_label.grid(row=3, column=0, columnspan=3, padx=5, pady=5) - - self.cm = CredentialsManager() # Replace with your actual CredentialsManager - - # Apply dark mode styling initially - self.configure_dark_mode() - - # Context Menu - self.context_menu = tk.Menu(master, tearoff=0) - self.context_menu.add_command(label="Copy", command=self.copy_selected_value) - - # Keep track of the right-clicked event - self.right_click_event = None - - # Bind right-click to show the context menu - self.credentials_tree.bind("", self.show_context_menu) - - def configure_dark_mode(self): - if self.dark_mode.get(): - self.master.configure(background="#24262C") - self.style.configure("TLabel", background="#24262C", foreground="#C9ADA7") - self.style.configure( - "TEntry", - background="#9A8C98", - foreground="#24262C", - insertcolor="#C9ADA7", - fieldbackground="#9A8C98", - ) - self.style.configure( - "TButton", - background="#9A8C98", - foreground="#24262C", - relief="flat", - borderwidth=0, - ) - self.style.configure( - "Treeview", - background="#24262C", - foreground="#C9ADA7", - fieldbackground="#33353F", - selectbackground="#9A8C98", - selectforeground="#24262C", - rowheight=30, - font=("Helvetica", 20, "bold"), - ) - self.style.configure( - "Treeview.Heading", - background="#33353F", - foreground="#C9ADA7", - relief="flat", - font=("Helvetica", 13, "bold"), - ) - - self.style.map( - "Treeview", - background=[("selected", "#9A8C98")], - foreground=[("selected", "#24262C")], - ) - else: - self.master.configure(background="white") - self.style.configure("TLabel", background="white", foreground="black") - self.style.configure("TEntry", background="white", foreground="black") - self.style.configure( - "TButton", background="SystemButtonFace", foreground="black" - ) - self.style.configure( - "Treeview", - background="white", - foreground="black", - fieldbackground="white", - ) - self.style.configure( - "Treeview.Heading", background="SystemButtonFace", foreground="black" - ) - - def toggle_dark_mode(self): - self.configure_dark_mode() - - def decrypt_credentials(self): - password = self.password_entry.get() - credentials_list = self.cm.get_credentials(password) - - if credentials_list: - self.credentials_tree.delete(*self.credentials_tree.get_children()) - for credentials in credentials_list: - email = credentials[0] if credentials else "" - email_password = credentials[1] if len(credentials) > 1 else "" - account_id = credentials[2] if len(credentials) > 2 else "" - account_password = credentials[3] if len(credentials) > 3 else "" - temp_links = self.cm.get_mega_links(email) - mega_links = temp_links if temp_links else "" - self.credentials_tree.insert( - "", - "end", - values=(email, email_password, account_id, account_password,mega_links), - ) - self.status_label.config(text="Credentials decrypted successfully.") - else: - self.status_label.config( - text="Failed to decrypt credentials. Please check the password and try again." - ) - - def show_context_menu(self, event): - self.right_click_event = event - try: - self.credentials_tree.selection_set( - self.credentials_tree.identify_row(event.y) - ) - self.context_menu.post(event.x_root, event.y_root) - finally: - self.context_menu.grab_release() - - def copy_selected_value(self): - if not self.right_click_event: - return - - region = self.credentials_tree.identify( - "region", self.right_click_event.x, self.right_click_event.y - ) - if region != "cell": - return - - column = self.credentials_tree.identify_column(self.right_click_event.x) - item = self.credentials_tree.identify_row(self.right_click_event.y) - if not column or not item: - return - - column_id = ( - int(column.replace("#", "")) - 1 - ) # Convert column index to zero-based - selected_value = self.credentials_tree.item(item)["values"][column_id] - - if selected_value: - self.master.clipboard_clear() - self.master.clipboard_append(selected_value) - self.master.update() # Keeps the clipboard data persistent - self.show_toast("Text copied!", "#28a745") - - def show_toast(self, message, color): - """ - Displays a toast message at a location that won't cover other widgets. - The message is rounded and styled. - """ - # Destroy any existing toast - if self.toast_canvas: - self.toast_canvas.destroy() - - # Avoid covering other widgets by positioning at the bottom-right corner - x = self.master.winfo_width() - 250 - y = self.master.winfo_height() - 80 - - # Create a new canvas for the toast - width, height = 200, 40 - radius = 20 - self.toast_canvas = tk.Canvas( - self.master, width=width, height=height, bg="#111111", highlightthickness=0 - ) - self.toast_canvas.place(x=x, y=y) - - # Draw a rounded rectangle using arcs and rectangles - self.toast_canvas.create_arc( - (0, 0, 2 * radius, 2 * radius), - start=90, - extent=90, - fill=color, - outline=color, - ) - self.toast_canvas.create_arc( - (width - 2 * radius, 0, width, 2 * radius), - start=0, - extent=90, - fill=color, - outline=color, - ) - self.toast_canvas.create_arc( - (0, height - 2 * radius, 2 * radius, height), - start=180, - extent=90, - fill=color, - outline=color, - ) - self.toast_canvas.create_arc( - (width - 2 * radius, height - 2 * radius, width, height), - start=270, - extent=90, - fill=color, - outline=color, - ) - self.toast_canvas.create_rectangle( - radius, 0, width - radius, height, fill=color, outline=color - ) - self.toast_canvas.create_rectangle( - 0, radius, width, height - radius, fill=color, outline=color - ) - - # Add the text to the toast - self.toast_message_id = self.toast_canvas.create_text( - width / 2, - height / 2, - text=message, - fill="white", - font=("Helvetica", 12, "bold"), - ) - - # Auto-destroy after 2500 ms - self.master.after(2500, self.toast_canvas.destroy) - - -root = tk.Tk() -viewer = CredentialsViewer(root) -root.mainloop() diff --git a/services/credentials_manager.py b/services/credentials_manager.py deleted file mode 100644 index bb898bf..0000000 --- a/services/credentials_manager.py +++ /dev/null @@ -1,79 +0,0 @@ -import os -import base64 -from cryptography.fernet import Fernet, InvalidToken -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC -import tinydb -from tinydb import Query - - -class CredentialsManager: - def __init__(self): - self.db = tinydb.TinyDB("credentials.json") - - def encrypt_credentials(self, password, credentials): - password = password.encode() # convert to bytes - salt = os.urandom(16) # generate a random salt - kdf = PBKDF2HMAC( - algorithm=hashes.SHA256(), length=32, salt=salt, iterations=100000 - ) - key = base64.urlsafe_b64encode(kdf.derive(password)) - f = Fernet(key) - encrypted_credentials = f.encrypt(credentials.encode()) - return encrypted_credentials, salt - - def decrypt_credentials(self, password, encrypted_credentials, salt): - try: - password = password.encode() # convert to bytes - kdf = PBKDF2HMAC( - algorithm=hashes.SHA256(), length=32, salt=salt, iterations=100000 - ) - key = base64.urlsafe_b64encode(kdf.derive(password)) - f = Fernet(key) - decrypted_credentials = f.decrypt(encrypted_credentials).decode() - return decrypted_credentials - except (ValueError, InvalidToken): - return None - - def store_credentials(self, password, email, encrypted_credentials_hex, salt_hex): - self.db.insert( - {"email": email, "credentials": encrypted_credentials_hex, "salt": salt_hex} - ) - - def get_credentials(self, password): - query = Query() - results = self.db.search(query.email.exists()) - decrypted_credentials = [] - for result in results: - try: - encrypted_credentials_hex = result["credentials"] - salt_hex = result["salt"] - encrypted_credentials = bytes.fromhex(encrypted_credentials_hex) - salt = bytes.fromhex(salt_hex) - decrypted_credential = self.decrypt_credentials( - password, encrypted_credentials, salt - ) - if decrypted_credential: - decrypted_credentials.append(decrypted_credential.split(":")) - except (ValueError, IndexError): - continue - return decrypted_credentials - - def update_account_mega(self,email,MegaLinkToAdd): - Emails = Query() - # self.db.search(Emails.email == f"{email}") - self.db.update({"megaLinks": [str(MegaLinkToAdd)]}, Emails.email == f"{email}") - return True - - - def get_mega_links(self, email): - Emails = Query() - results = self.db.search(Emails.email == f"{email}") - if len(results) == 0: - return ["No Links found"] - else: - result = results[0] - if "megaLinks" in result: - return result["megaLinks"] - else: - return ["No megaLinks found"] diff --git a/services/gui.py b/services/gui.py new file mode 100644 index 0000000..9ad5e90 --- /dev/null +++ b/services/gui.py @@ -0,0 +1,382 @@ +import os +import base64 +from cryptography.fernet import Fernet, InvalidToken +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +import tinydb +from tinydb import Query +import tkinter as tk +from tkinter import ttk + + +class CredentialsManager: + def __init__(self): + self.db = tinydb.TinyDB("credentials.json") + + def encrypt_credentials(self, password, credentials): + password = password.encode() # convert to bytes + salt = os.urandom(16) # generate a random salt + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), length=32, salt=salt, iterations=100000 + ) + key = base64.urlsafe_b64encode(kdf.derive(password)) + f = Fernet(key) + encrypted_credentials = f.encrypt(credentials.encode()) + return encrypted_credentials, salt + + def decrypt_credentials(self, password, encrypted_credentials, salt): + try: + password = password.encode() # convert to bytes + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), length=32, salt=salt, iterations=100000 + ) + key = base64.urlsafe_b64encode(kdf.derive(password)) + f = Fernet(key) + decrypted_credentials = f.decrypt(encrypted_credentials).decode() + return decrypted_credentials + except (ValueError, InvalidToken): + return None + + def store_credentials(self, password, email, encrypted_credentials_hex, salt_hex): + self.db.insert( + {"email": email, "credentials": encrypted_credentials_hex, "salt": salt_hex} + ) + + def get_credentials(self, password): + query = Query() + results = self.db.search(query.email.exists()) + decrypted_credentials = [] + for result in results: + try: + encrypted_credentials_hex = result["credentials"] + salt_hex = result["salt"] + encrypted_credentials = bytes.fromhex(encrypted_credentials_hex) + salt = bytes.fromhex(salt_hex) + decrypted_credential = self.decrypt_credentials( + password, encrypted_credentials, salt + ) + if decrypted_credential: + decrypted_credentials.append(decrypted_credential.split(":")) + except (ValueError, IndexError): + continue + return decrypted_credentials + + def update_account_mega(self, email, MegaLinkToAdd): + Emails = Query() + self.db.update({"megaLinks": [str(MegaLinkToAdd)]}, Emails.email == f"{email}") + return True + + def get_mega_links(self, email): + Emails = Query() + results = self.db.search(Emails.email == f"{email}") + if len(results) == 0: + return ["No Links found"] + else: + result = results[0] + if "megaLinks" in result: + return result["megaLinks"] + else: + return ["No megaLinks found"] + + +class CredentialsViewer: + """ + Theme: + Black #24262C + Pink #C9ADA7 + Rose quartz #9A8C98 + """ + + def __init__(self, master): + self.master = master + master.title("Credentials Viewer") + + self.toast_canvas = None # Canvas for toast messages + self.toast_message_id = None # Text id for the toast message + + self.dark_mode = tk.BooleanVar() + self.dark_mode.set(True) # Start in dark mode + + self.style = ttk.Style() + self.configure_dark_mode() + ttk.Style().configure( + "green/black.TLabel", + foreground="#9A8C98", + background="#111111", + font=("Helvetica", 10, "bold"), + ) + ttk.Style().configure( + "green/black.TButton", foreground="#9A8C98", background="#111111" + ) + ttk.Style().configure( + "green/black.TEntry", foreground="#9A8C98", background="#111111" + ) + ttk.Style().configure( + "green/black.TCheckbutton", foreground="#9A8C98", background="#111111" + ) + ttk.Style().configure( + "green/black.THeading", foreground="#9A8C98", background="#111111" + ) + + master.configure(background="#24262C") + + self.dark_mode_checkbox = ttk.Checkbutton( + master, + style="green/black.TCheckbutton", + text="Dark Mode", + variable=self.dark_mode, + command=self.toggle_dark_mode, + ) + self.dark_mode_checkbox.grid(row=0, column=0, padx=5, pady=5) + + self.password_label = ttk.Label(master, text="Password:") + self.password_label.grid(row=1, column=0, padx=5, pady=5) + + self.password_entry = ttk.Entry(master, show="*", style="green/black.TEntry") + self.password_entry.grid(row=1, column=1, padx=5, pady=5) + + self.decrypt_button = ttk.Button( + master, + text="Decrypt", + command=self.decrypt_credentials, + style="green/black.TButton", + ) + self.decrypt_button.grid(row=1, column=2, padx=5, pady=5) + + self.credentials_tree = ttk.Treeview( + master, + style="green/black.TLabel", + columns=( + "Email", + "Email Password", + "Account ID", + "Account Password", + "Mega LINKs", + ), + show="headings", + ) + self.credentials_tree.heading("Email", text="Email") + self.credentials_tree.heading("Email Password", text="Email Password") + self.credentials_tree.heading("Account ID", text="Account ID") + self.credentials_tree.heading("Account Password", text="Account Password") + self.credentials_tree.heading("#5", text="MEGA DL Links") + self.credentials_tree.grid(row=2, column=0, columnspan=3, padx=5, pady=5) + + self.status_label = ttk.Label(master, text="") + self.status_label.grid(row=3, column=0, columnspan=3, padx=5, pady=5) + + self.cm = CredentialsManager() + + # Apply dark mode styling initially + self.configure_dark_mode() + + # Context Menu + self.context_menu = tk.Menu(master, tearoff=0) + self.context_menu.add_command(label="Copy", command=self.copy_selected_value) + + # Keep track of the right-clicked event + self.right_click_event = None + + # Bind right-click to show the context menu + self.credentials_tree.bind("", self.show_context_menu) + + def configure_dark_mode(self): + if self.dark_mode.get(): + self.master.configure(background="#24262C") + self.style.configure("TLabel", background="#24262C", foreground="#C9ADA7") + self.style.configure( + "TEntry", + background="#9A8C98", + foreground="#24262C", + insertcolor="#C9ADA7", + fieldbackground="#9A8C98", + ) + self.style.configure( + "TButton", + background="#9A8C98", + foreground="#24262C", + relief="flat", + borderwidth=0, + ) + self.style.configure( + "Treeview", + background="#24262C", + foreground="#C9ADA7", + fieldbackground="#33353F", + selectbackground="#9A8C98", + selectforeground="#24262C", + rowheight=30, + font=("Helvetica", 20, "bold"), + ) + self.style.configure( + "Treeview.Heading", + background="#33353F", + foreground="#C9ADA7", + relief="flat", + font=("Helvetica", 13, "bold"), + ) + + self.style.map( + "Treeview", + background=[("selected", "#9A8C98")], + foreground=[("selected", "#24262C")], + ) + else: + self.master.configure(background="white") + self.style.configure("TLabel", background="white", foreground="black") + self.style.configure("TEntry", background="white", foreground="black") + self.style.configure( + "TButton", background="SystemButtonFace", foreground="black" + ) + self.style.configure( + "Treeview", + background="white", + foreground="black", + fieldbackground="white", + ) + self.style.configure( + "Treeview.Heading", background="SystemButtonFace", foreground="black" + ) + + def toggle_dark_mode(self): + self.configure_dark_mode() + + def decrypt_credentials(self): + password = self.password_entry.get() + credentials_list = self.cm.get_credentials(password) + + if credentials_list: + self.credentials_tree.delete(*self.credentials_tree.get_children()) + for credentials in credentials_list: + email = credentials[0] if credentials else "" + email_password = credentials[1] if len(credentials) > 1 else "" + account_id = credentials[2] if len(credentials) > 2 else "" + account_password = credentials[3] if len(credentials) > 3 else "" + temp_links = self.cm.get_mega_links(email) + mega_links = temp_links if temp_links else "" + self.credentials_tree.insert( + "", + "end", + values=( + email, + email_password, + account_id, + account_password, + mega_links, + ), + ) + self.status_label.config(text="Credentials decrypted successfully.") + else: + self.status_label.config( + text="Failed to decrypt credentials. Please check the password and try again." + ) + + def show_context_menu(self, event): + self.right_click_event = event + try: + self.credentials_tree.selection_set( + self.credentials_tree.identify_row(event.y) + ) + self.context_menu.post(event.x_root, event.y_root) + finally: + self.context_menu.grab_release() + + def copy_selected_value(self): + if not self.right_click_event: + return + + region = self.credentials_tree.identify( + "region", self.right_click_event.x, self.right_click_event.y + ) + if region != "cell": + return + + column = self.credentials_tree.identify_column(self.right_click_event.x) + item = self.credentials_tree.identify_row(self.right_click_event.y) + if not column or not item: + return + + column_id = ( + int(column.replace("#", "")) - 1 + ) # Convert column index to zero-based + selected_value = self.credentials_tree.item(item)["values"][column_id] + + if selected_value: + self.master.clipboard_clear() + self.master.clipboard_append(selected_value) + self.master.update() # Keeps the clipboard data persistent + self.show_toast("Text copied!", "#28a745") + + def show_toast(self, message, color): + """ + Displays a toast message at a location that won't cover other widgets. + The message is rounded and styled. + """ + # Destroy any existing toast + if self.toast_canvas: + self.toast_canvas.destroy() + + # Avoid covering other widgets by positioning at the bottom-right corner + x = self.master.winfo_width() - 250 + y = self.master.winfo_height() - 80 + + # Create a new canvas for the toast + width, height = 200, 40 + radius = 20 + self.toast_canvas = tk.Canvas( + self.master, width=width, height=height, bg="#111111", highlightthickness=0 + ) + self.toast_canvas.place(x=x, y=y) + + # Draw a rounded rectangle using arcs and rectangles + self.toast_canvas.create_arc( + (0, 0, 2 * radius, 2 * radius), + start=90, + extent=90, + fill=color, + outline=color, + ) + self.toast_canvas.create_arc( + (width - 2 * radius, 0, width, 2 * radius), + start=0, + extent=90, + fill=color, + outline=color, + ) + self.toast_canvas.create_arc( + (0, height - 2 * radius, 2 * radius, height), + start=180, + extent=90, + fill=color, + outline=color, + ) + self.toast_canvas.create_arc( + (width - 2 * radius, height - 2 * radius, width, height), + start=270, + extent=90, + fill=color, + outline=color, + ) + self.toast_canvas.create_rectangle( + radius, 0, width - radius, height, fill=color, outline=color + ) + self.toast_canvas.create_rectangle( + 0, radius, width, height - radius, fill=color, outline=color + ) + + # Add the text to the toast + self.toast_message_id = self.toast_canvas.create_text( + width / 2, + height / 2, + text=message, + fill="white", + font=("Helvetica", 12, "bold"), + ) + + # Auto-destroy after 2500 ms + self.master.after(2500, self.toast_canvas.destroy) + + +root = tk.Tk() +viewer = CredentialsViewer(root) +root.mainloop() diff --git a/utilities/etc.py b/utilities/etc.py index 123dc12..fb9eb02 100644 --- a/utilities/etc.py +++ b/utilities/etc.py @@ -7,6 +7,7 @@ import sys from mega import Mega import psutil +import getpass from utilities.types import Colours, Credentials @@ -72,6 +73,23 @@ def reinstall_tenacity(): # sourcery skip: extract-method sys.exit(1) +def get_matching_password(): + """ + Prompts the user to enter and confirm a password. + Returns the password if both inputs match. + Prompts again if the passwords don't match. + """ + while True: + password = getpass.getpass("Enter password to encrypt credentials: ") + password_2 = getpass.getpass("Re-enter password to confirm credentials: ") + + if password == password_2: + print("Passwords match.") + return password + else: + print("Passwords do not match. Please try again.") + + def kill_process(matches: list): """Kills processes.""" for process in psutil.process_iter(): diff --git a/utilities/fs.py b/utilities/fs.py index 8f3ff62..9a45105 100644 --- a/utilities/fs.py +++ b/utilities/fs.py @@ -7,7 +7,6 @@ from utilities.etc import p_print from utilities.types import Colours, Credentials, Config -from typing import Union CONFIG_FILE = "config.json"