Merge branch 'main' into mac-compoatibility-enhancements
This commit is contained in:
34
README.md
34
README.md
@@ -16,19 +16,20 @@ and should prevent most any random attacker on your network from being able to
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
To avoid running into issues later with your default python installs, it's recommended to use a py virtual env for doing this. Go to your desired test directory, and:
|
||||||
```
|
```
|
||||||
pip3 -r requirements.txt
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
git clone https://github.com/osresearch/hcpy
|
||||||
|
cd hcpy
|
||||||
|
pip3 install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Install the Python dependencies; the `sslpsk` one is a little weird
|
Install the Python dependencies; the `sslpsk` one is a little weird
|
||||||
and we might need to revisit it later.
|
and we might need to revisit it later.
|
||||||
|
|
||||||
### For Mac Users
|
|
||||||
To avoid running into issues later with your default python installs, it's recommended to use a py virtual env for doing this. Go to your desired test directory, and:
|
|
||||||
1. python3 -m venv venv
|
|
||||||
1. source venv/bin/activate
|
|
||||||
1. clone this repo
|
|
||||||
|
|
||||||
|
### For Mac Users
|
||||||
Installing `sslpsk` needs some extra steps:
|
Installing `sslpsk` needs some extra steps:
|
||||||
|
|
||||||
1. The openssl package installed via brew: `brew install openssl`, and
|
1. The openssl package installed via brew: `brew install openssl`, and
|
||||||
@@ -39,7 +40,7 @@ Installing `sslpsk` needs some extra steps:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
```
|
```bash
|
||||||
hc-login $USERNAME $PASSWORD > config.json
|
hc-login $USERNAME $PASSWORD > config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -55,10 +56,9 @@ the resulting configuration JSON file *should* be sufficient to
|
|||||||
connect to the devices on your local network, assuming that
|
connect to the devices on your local network, assuming that
|
||||||
your mDNS or DNS server resolves the names correctly.
|
your mDNS or DNS server resolves the names correctly.
|
||||||
|
|
||||||
|
|
||||||
## Home Connect to MQTT
|
## Home Connect to MQTT
|
||||||
|
|
||||||
```
|
```bash
|
||||||
hc2mqtt config.json
|
hc2mqtt config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ library.
|
|||||||
|
|
||||||
Example message published to `homeconnect/dishwasher`:
|
Example message published to `homeconnect/dishwasher`:
|
||||||
|
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
"state": "Run",
|
"state": "Run",
|
||||||
"door": "Closed",
|
"door": "Closed",
|
||||||
@@ -98,7 +98,7 @@ Example message published to `homeconnect/dishwasher`:
|
|||||||
<details>
|
<details>
|
||||||
<summary>Full state information</summary>
|
<summary>Full state information</summary>
|
||||||
|
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
'AllowBackendConnection': False,
|
'AllowBackendConnection': False,
|
||||||
'BackendConnected': False,
|
'BackendConnected': False,
|
||||||
@@ -163,8 +163,8 @@ Example message published to `homeconnect/dishwasher`:
|
|||||||
'SilenceOnDemand': False
|
'SilenceOnDemand': False
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</details>
|
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
### Clothes washer
|
### Clothes washer
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ binary data over the websocket (type 0x82).
|
|||||||
|
|
||||||
Example message published to `homeconnect/washer`:
|
Example message published to `homeconnect/washer`:
|
||||||
|
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
"state": "Ready",
|
"state": "Ready",
|
||||||
"door": "Closed",
|
"door": "Closed",
|
||||||
@@ -194,7 +194,7 @@ Example message published to `homeconnect/washer`:
|
|||||||
<details>
|
<details>
|
||||||
<summary>Full state information</summary>
|
<summary>Full state information</summary>
|
||||||
|
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
'BackendConnected': False,
|
'BackendConnected': False,
|
||||||
'CustomerEnergyManagerPaired': False,
|
'CustomerEnergyManagerPaired': False,
|
||||||
@@ -256,8 +256,8 @@ Example message published to `homeconnect/washer`:
|
|||||||
'SelectedProgram': 28718
|
'SelectedProgram': 28718
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</details>
|
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
### Coffee Machine
|
### Coffee Machine
|
||||||
|
|
||||||
@@ -268,7 +268,7 @@ The coffee machine needs a better mapping to MQTT messages.
|
|||||||
<details>
|
<details>
|
||||||
<summary>Full state information</summary>
|
<summary>Full state information</summary>
|
||||||
|
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
'LastSelectedBeverage': 8217,
|
'LastSelectedBeverage': 8217,
|
||||||
'LocalControlActive': False,
|
'LocalControlActive': False,
|
||||||
@@ -361,8 +361,8 @@ The coffee machine needs a better mapping to MQTT messages.
|
|||||||
'ProcessPhase': 'None'
|
'ProcessPhase': 'None'
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</details>
|
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## FRIDA tools
|
## FRIDA tools
|
||||||
|
|
||||||
|
|||||||
39
hc-login
39
hc-login
@@ -4,7 +4,7 @@
|
|||||||
# A really nice walk through of how it works is:
|
# A really nice walk through of how it works is:
|
||||||
# https://auth0.com/docs/get-started/authentication-and-authorization-flow/call-your-api-using-the-authorization-code-flow-with-pkce
|
# https://auth0.com/docs/get-started/authentication-and-authorization-flow/call-your-api-using-the-authorization-code-flow-with-pkce
|
||||||
import requests
|
import requests
|
||||||
from urllib.parse import urlparse, parse_qs, urlencode
|
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
||||||
from lxml import html
|
from lxml import html
|
||||||
import io
|
import io
|
||||||
import re
|
import re
|
||||||
@@ -13,6 +13,7 @@ import json
|
|||||||
from time import time
|
from time import time
|
||||||
from base64 import b64decode as base64_decode
|
from base64 import b64decode as base64_decode
|
||||||
from base64 import urlsafe_b64encode as base64url_encode
|
from base64 import urlsafe_b64encode as base64url_encode
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
from Crypto.Random import get_random_bytes
|
from Crypto.Random import get_random_bytes
|
||||||
from Crypto.Hash import SHA256
|
from Crypto.Hash import SHA256
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
@@ -124,44 +125,40 @@ while True:
|
|||||||
r = session.get(preauth_url, allow_redirects=False)
|
r = session.get(preauth_url, allow_redirects=False)
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
break
|
break
|
||||||
if r.status_code == 302 or r.status_code == 301:
|
if r.status_code > 300 and r.status_code < 400:
|
||||||
preauth_url = r.headers["location"]
|
preauth_url = r.headers["location"]
|
||||||
|
# Make relative locations absolute
|
||||||
|
if not bool(urlparse(preauth_url).netloc):
|
||||||
|
preauth_url = singlekey_host + preauth_url
|
||||||
continue
|
continue
|
||||||
print(f"2: {preauth_url=}: failed to fetch {r} {r.text}", file=sys.stderr)
|
print(f"2: {preauth_url=}: failed to fetch {r} {r.text}", file=sys.stderr)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
# get the ReturnUrl from the response
|
# get the ReturnUrl from the response
|
||||||
query = parse_qs(urlparse(preauth_url).query)
|
query = parse_qs(urlparse(preauth_url).query)
|
||||||
return_url = query["ReturnUrl"][0]
|
return_url = query["returnUrl"][0]
|
||||||
debug(f"{return_url=}")
|
debug(f"{return_url=}")
|
||||||
|
|
||||||
|
if "X-CSRF-FORM-TOKEN" in r.cookies:
|
||||||
headers["RequestVerificationToken"] = r.cookies["X-CSRF-FORM-TOKEN"]
|
headers["RequestVerificationToken"] = r.cookies["X-CSRF-FORM-TOKEN"]
|
||||||
session.headers.update(headers)
|
session.headers.update(headers)
|
||||||
|
|
||||||
debug("--------")
|
debug("--------")
|
||||||
|
|
||||||
valid_url = singlekey_host + '/auth/api/v1/authentication/UserExists'
|
soup = BeautifulSoup(r.text, 'html.parser')
|
||||||
auth_url = singlekey_host + '/auth/api/v1/authentication/login'
|
requestVerificationToken = soup.find('input', {'name': '__RequestVerificationToken'}).get('value')
|
||||||
|
r = session.post(preauth_url, data={"UserIdentifierInput.EmailInput.StringValue": email, "__RequestVerificationToken": requestVerificationToken }, allow_redirects=False)
|
||||||
|
|
||||||
r = session.post(valid_url, json={"username": email})
|
password_url = r.headers['location']
|
||||||
debug(f"{valid_url=}: {r} {r.text}")
|
if not bool(urlparse(password_url).netloc):
|
||||||
|
password_url = singlekey_host + password_url
|
||||||
|
|
||||||
|
r = session.get(password_url, allow_redirects=False)
|
||||||
|
soup = BeautifulSoup(r.text, 'html.parser')
|
||||||
|
requestVerificationToken = soup.find('input', {'name': '__RequestVerificationToken'}).get('value')
|
||||||
|
|
||||||
login_fields = {
|
r = session.post(password_url, data={"Password": password, "RememberMe": "false", "__RequestVerificationToken": requestVerificationToken }, allow_redirects=False)
|
||||||
"username": email,
|
|
||||||
"password": password,
|
|
||||||
"keepMeSignedIn": False,
|
|
||||||
"returnUrl": return_url,
|
|
||||||
}
|
|
||||||
|
|
||||||
r = session.post(auth_url, json=login_fields, allow_redirects=False)
|
|
||||||
|
|
||||||
if r.status_code != 200:
|
|
||||||
debug(f"auth failed: {auth_url=}, {login_fields=} {r} {r.text}")
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
debug(f"{auth_url=}, {r} {r.text}")
|
|
||||||
return_url = json.loads(r.text)["returnUrl"]
|
|
||||||
if return_url.startswith("/"):
|
if return_url.startswith("/"):
|
||||||
return_url = singlekey_host + return_url
|
return_url = singlekey_host + return_url
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
bs4
|
||||||
|
requests
|
||||||
pycryptodome
|
pycryptodome
|
||||||
websocket-client
|
websocket-client
|
||||||
sslpsk
|
sslpsk
|
||||||
|
|||||||
Reference in New Issue
Block a user