From 3bae5548c07e0cbbf1e3c2a3c0d03ee3dff86614 Mon Sep 17 00:00:00 2001 From: Nighthawk Date: Mon, 1 Jul 2024 20:46:58 -0400 Subject: [PATCH] Initial commit --- README.md | 57 ++++++++++++++++ eero_auth.py | 102 +++++++++++++++++++++++++++++ eero_cli.py | 164 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 + 4 files changed, 326 insertions(+) create mode 100644 eero_auth.py create mode 100644 eero_cli.py create mode 100644 requirements.txt diff --git a/README.md b/README.md index ed467df..3794061 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,59 @@ # eero-py eero-py is a Python-based CLI tool designed to interact with the eero API. This tool allows users dump information about their eero networks through simple command-line. + +## Features + +- Log into your eero account, using either your e-mail or phone number. +- 2FA support. +- Securely stores your session information. +- Dump and monitor your eero network + +## Installation + +To get started, clone the repository and install the required dependencies: + +``` +git clone https://github.com/yourusername/eero-py.git +cd eero-py +pip install -r requirements.txt +``` + +## Usage + +- **Account Information** +``` +python eero_cli.py account +``` +Retrieves and displays your account information. + +- **List Devices:** +``` +python eero_cli.py devices +``` +Lists all devices connected to your eero network. + +- **Get Network Status:** +``` +python eero_cli.py networks +``` +Displays the current status of your eero network. + +- **Display Session Token** +``` +python eero_cli.py session +``` +Displays the encrypted session token if it exists. + +## Output +- ``` +--output .json | .csv +``` +You can save the output of a quary by appending the --output switch after the command. JSON and CSV are supported. + +## Contributing + +Contributions are welcome! Please fork the repository and submit a pull request for review. + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/eero_auth.py b/eero_auth.py new file mode 100644 index 0000000..a2330b5 --- /dev/null +++ b/eero_auth.py @@ -0,0 +1,102 @@ +import requests +import os +import click +import base64 +from getpass import getpass +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import padding, hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.backends import default_backend + +BASE_URL = "https://api-user.e2ro.com/2.2/" +SESSION_FILE = "eero_session" + +def derive_key(passphrase, salt): + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=100000, + backend=default_backend() + ) + return kdf.derive(passphrase.encode()) + +def encrypt_data(data, passphrase): + salt = os.urandom(16) + key = derive_key(passphrase, salt) + iv = os.urandom(16) + cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) + encryptor = cipher.encryptor() + padder = padding.PKCS7(128).padder() + padded_data = padder.update(data.encode()) + padder.finalize() + ct = encryptor.update(padded_data) + encryptor.finalize() + return base64.b64encode(salt + iv + ct).decode() + +def decrypt_data(encrypted_data, passphrase): + encrypted_data = base64.b64decode(encrypted_data.encode()) + salt = encrypted_data[:16] + iv = encrypted_data[16:32] + ct = encrypted_data[32:] + key = derive_key(passphrase, salt) + cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) + decryptor = cipher.decryptor() + padded_data = decryptor.update(ct) + decryptor.finalize() + unpadder = padding.PKCS7(128).unpadder() + data = unpadder.update(padded_data) + unpadder.finalize() + return data.decode() + +class EeroClient: + def __init__(self, session_token=""): + self.session_token = session_token + self.headers = { + "Content-Type": "application/json", + } + if session_token: + self.headers["Cookie"] = f"s={session_token}" + + def login(self, login): + url = f"{BASE_URL}login" + payload = {"login": login} + response = requests.post(url, json=payload, headers=self.headers) + if response.status_code == 200: + return response.json()["data"] + else: + raise Exception(f"Login failed: {response.text}") + + def login_verify(self, session_token, verification_code): + url = f"{BASE_URL}login/verify" + payload = {"code": verification_code} + headers = self.headers.copy() + headers["Cookie"] = f"s={session_token}" + response = requests.post(url, json=payload, headers=headers) + if response.status_code == 200: + return response.json() + else: + raise Exception(f"Verification failed: {response.text}") + +@click.command() +@click.option('--session-file', default=SESSION_FILE, help='Session file to load/save session token') +def auth(session_file): + while True: + passphrase = getpass("Enter passphrase for encrypting session token: ") + client = EeroClient() + user_id = input("Enter Eero login ID (phone or email address): ").strip().lower() + + try: + login_data = client.login(user_id) + session_token = login_data["user_token"] + verification_code = input("Enter OTP from Email or SMS: ").strip() + client.login_verify(session_token, verification_code) + encrypted_token = encrypt_data(session_token, passphrase) + with open(session_file, 'w') as file: + file.write(encrypted_token) + print("Authentication successful.") + break # Exit the loop upon successful authentication + except Exception as e: + print(f"Error: {e}") + retry = input("Login failed. Would you like to try again? (yes/no): ").strip().lower() + if retry != 'yes': + break # Exit the loop if the user does not want to retry + +if __name__ == "__main__": + auth() diff --git a/eero_cli.py b/eero_cli.py new file mode 100644 index 0000000..f7eb8c6 --- /dev/null +++ b/eero_cli.py @@ -0,0 +1,164 @@ +import requests +import os +import json +import csv +import click +import base64 +from getpass import getpass +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import padding, hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.backends import default_backend + +BASE_URL = "https://api-user.e2ro.com/2.2/" +SESSION_FILE = "eero_session" + +def derive_key(passphrase, salt): + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=100000, + backend=default_backend() + ) + return kdf.derive(passphrase.encode()) + +def encrypt_data(data, passphrase): + salt = os.urandom(16) # Generate a unique salt + key = derive_key(passphrase, salt) + iv = os.urandom(16) # Generate a unique IV + cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) + encryptor = cipher.encryptor() + padder = padding.PKCS7(128).padder() + padded_data = padder.update(data.encode()) + padder.finalize() + ct = encryptor.update(padded_data) + encryptor.finalize() + return base64.b64encode(salt + iv + ct).decode() + +def decrypt_data(encrypted_data, passphrase): + encrypted_data = base64.b64decode(encrypted_data.encode()) + salt = encrypted_data[:16] # Extract the salt + iv = encrypted_data[16:32] # Extract the IV + ct = encrypted_data[32:] # Extract the ciphertext + key = derive_key(passphrase, salt) + cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) + decryptor = cipher.decryptor() + padded_data = decryptor.update(ct) + decryptor.finalize() + unpadder = padding.PKCS7(128).unpadder() + data = unpadder.update(padded_data) + unpadder.finalize() + return data.decode() + +class EeroClient: + def __init__(self, session_token=""): + self.session_token = session_token + self.headers = { + "Content-Type": "application/json", + } + if session_token: + self.headers["Cookie"] = f"s={session_token}" + + def get_account(self): + url = f"{BASE_URL}account" + response = requests.get(url, headers=self.headers) + if response.status_code == 200: + return response.json()["data"] + else: + raise Exception(f"Account retrieval failed: {response.text}") + + def get_networks(self): + account_data = self.get_account() + return account_data["networks"]["data"] + + def get_devices(self): + networks = self.get_networks() + devices = [] + for network in networks: + network_id = network["url"].split("/")[-1] + url = f"{BASE_URL}networks/{network_id}/devices" + response = requests.get(url, headers=self.headers) + if response.status_code == 200: + devices.extend(response.json()["data"]) + else: + raise Exception(f"Device retrieval failed: {response.text}") + return devices + +@click.group() +@click.option('--session-file', default=SESSION_FILE, help='Session file to load/save session token') +@click.pass_context +def cli(ctx, session_file): + ctx.ensure_object(dict) + ctx.obj['SESSION_FILE'] = session_file + passphrase = getpass("Enter passphrase for decrypting session token: ") + if os.path.exists(session_file): + with open(session_file, 'r') as file: + encrypted_token = file.read().strip() + session_token = decrypt_data(encrypted_token, passphrase) + ctx.obj['CLIENT'] = EeroClient(session_token) + else: + ctx.obj['CLIENT'] = EeroClient() + +def save_to_file(data, output_file): + if output_file.endswith('.json'): + with open(output_file, 'w') as f: + json.dump(data, f, indent=2) + print(f"Data saved to {output_file}") + elif output_file.endswith('.csv'): + with open(output_file, 'w', newline='') as f: + writer = csv.writer(f) + if isinstance(data, list): + headers = data[0].keys() + writer.writerow(headers) + for row in data: + writer.writerow(row.values()) + else: + writer.writerow(data.keys()) + writer.writerow(data.values()) + print(f"Data saved to {output_file}") + else: + print(f"Unsupported file format: {output_file}") + +@cli.command() +@click.option('--output', default=None, help='Output file to save the data in JSON or CSV format') +@click.pass_context +def account(ctx, output): + client = ctx.obj['CLIENT'] + account_data = client.get_account() + if output: + save_to_file(account_data, output) + else: + print(json.dumps(account_data, indent=2)) + +@cli.command() +@click.option('--output', default=None, help='Output file to save the data in JSON or CSV format') +@click.pass_context +def devices(ctx, output): + client = ctx.obj['CLIENT'] + devices_data = client.get_devices() + if output: + save_to_file(devices_data, output) + else: + print(json.dumps(devices_data, indent=2)) + +@cli.command() +@click.option('--output', default=None, help='Output file to save the data in JSON or CSV format') +@click.pass_context +def networks(ctx, output): + client = ctx.obj['CLIENT'] + networks_data = client.get_networks() + if output: + save_to_file(networks_data, output) + else: + print(json.dumps(networks_data, indent=2)) + +@cli.command() +@click.pass_context +def session(ctx): + session_file = ctx.obj['SESSION_FILE'] + if os.path.exists(session_file): + with open(session_file, 'r') as file: + encrypted_token = file.read().strip() + print(f"Encrypted token: {encrypted_token}") + else: + print("No session file found.") + +if __name__ == "__main__": + cli() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1493752 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +click +cryptography +requests \ No newline at end of file