Initial commit

This commit is contained in:
Nighthawk
2024-07-01 20:46:58 -04:00
parent 6b78328fda
commit 3bae5548c0
4 changed files with 326 additions and 0 deletions
+57
View File
@@ -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 <filename>.json | <filename>.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.
+102
View File
@@ -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()
+164
View File
@@ -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()
+3
View File
@@ -0,0 +1,3 @@
click
cryptography
requests