Skip to content

Commit 067989c

Browse files
dsypniewskibdraco
andauthored
Fix lock encryption key retrieval (#236)
Co-authored-by: J. Nick Koston <[email protected]>
1 parent 343f6cc commit 067989c

File tree

6 files changed

+90
-75
lines changed

6 files changed

+90
-75
lines changed

requirements.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1+
aiohttp>=3.9.5
12
bleak>=0.17.0
23
bleak-retry-connector>=2.9.0
34
cryptography>=38.0.3
4-
boto3>=1.20.24
5-
requests>=2.28.1

requirements_dev.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
pytest-asyncio
22
pytest-cov
3+
aiohttp>=3.9.5
34
bleak>=0.17.0
45
bleak-retry-connector>=3.4.0
56
cryptography>=38.0.3
6-
boto3>=1.20.24
7-
requests>=2.28.1

setup.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44
name="PySwitchbot",
55
packages=["switchbot", "switchbot.devices", "switchbot.adv_parsers"],
66
install_requires=[
7+
"aiohttp>=3.9.5",
78
"bleak>=0.19.0",
89
"bleak-retry-connector>=3.4.0",
910
"cryptography>=39.0.0",
1011
"pyOpenSSL>=23.0.0",
11-
"boto3>=1.20.24",
12-
"requests>=2.28.1",
1312
],
1413
version="0.44.1",
1514
description="A library to communicate with Switchbot",

switchbot/api_config.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
# Those values have been obtained from the following files in SwitchBot Android app
22
# That's how you can verify them yourself
33
# /assets/switchbot_config.json
4-
# /res/raw/amplifyconfiguration.json
5-
# /res/raw/awsconfiguration.json
64

7-
SWITCHBOT_APP_API_BASE_URL = "https://l9ren7efdj.execute-api.us-east-1.amazonaws.com"
8-
SWITCHBOT_APP_COGNITO_POOL = {
9-
"PoolId": "us-east-1_x1fixo5LC",
10-
"AppClientId": "66r90hdllaj4nnlne4qna0muls",
11-
"AppClientSecret": "1v3v7vfjsiggiupkeuqvsovg084e3msbefpj9rgh611u30uug6t8",
12-
"Region": "us-east-1",
13-
}
5+
SWITCHBOT_APP_API_BASE_URL = "api.switchbot.net"
6+
SWITCHBOT_APP_CLIENT_ID = "5nnwmhmsa9xxskm14hd85lm9bm"

switchbot/const.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010
DEFAULT_SCAN_TIMEOUT = 5
1111

1212

13+
class SwitchbotApiError(RuntimeError):
14+
"""Raised when API call fails.
15+
16+
This exception inherits from RuntimeError to avoid breaking existing code
17+
but will be changed to Exception in a future release.
18+
"""
19+
20+
1321
class SwitchbotAuthenticationError(RuntimeError):
1422
"""Raised when authentication fails.
1523

switchbot/devices/lock.py

Lines changed: 77 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
"""Library to handle connection with Switchbot Lock."""
22
from __future__ import annotations
33

4-
import base64
5-
import hashlib
6-
import hmac
7-
import json
4+
import asyncio
85
import logging
96
import time
107
from typing import Any
118

12-
import boto3
13-
import requests
9+
import aiohttp
1410
from bleak.backends.device import BLEDevice
1511
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
1612

17-
from ..api_config import SWITCHBOT_APP_API_BASE_URL, SWITCHBOT_APP_COGNITO_POOL
13+
from ..api_config import SWITCHBOT_APP_API_BASE_URL, SWITCHBOT_APP_CLIENT_ID
1814
from ..const import (
1915
LockStatus,
2016
SwitchbotAccountConnectionError,
17+
SwitchbotApiError,
2118
SwitchbotAuthenticationError,
2219
)
2320
from .device import SwitchbotDevice, SwitchbotOperationError
@@ -86,77 +83,97 @@ async def verify_encryption_key(
8683

8784
return lock_info is not None
8885

86+
@staticmethod
87+
async def api_request(
88+
session: aiohttp.ClientSession,
89+
subdomain: str,
90+
path: str,
91+
data: dict = None,
92+
headers: dict = None,
93+
) -> dict:
94+
url = f"https://{subdomain}.{SWITCHBOT_APP_API_BASE_URL}/{path}"
95+
async with session.post(url, json=data, headers=headers) as result:
96+
if result.status > 299:
97+
raise SwitchbotApiError(
98+
f"Unexpected status code returned by SwitchBot API: {result.status}"
99+
)
100+
101+
response = await result.json()
102+
if response["statusCode"] != 100:
103+
raise SwitchbotApiError(
104+
f"{response['message']}, status code: {response['statusCode']}"
105+
)
106+
107+
return response["body"]
108+
109+
# Old non-async method preserved for backwards compatibility
89110
@staticmethod
90111
def retrieve_encryption_key(device_mac: str, username: str, password: str):
112+
async def async_fn():
113+
async with aiohttp.ClientSession() as session:
114+
return await SwitchbotLock.async_retrieve_encryption_key(
115+
session, device_mac, username, password
116+
)
117+
118+
return asyncio.run(async_fn())
119+
120+
@staticmethod
121+
async def async_retrieve_encryption_key(
122+
session: aiohttp.ClientSession, device_mac: str, username: str, password: str
123+
) -> dict:
91124
"""Retrieve lock key from internal SwitchBot API."""
92125
device_mac = device_mac.replace(":", "").replace("-", "").upper()
93-
msg = bytes(username + SWITCHBOT_APP_COGNITO_POOL["AppClientId"], "utf-8")
94-
secret_hash = base64.b64encode(
95-
hmac.new(
96-
SWITCHBOT_APP_COGNITO_POOL["AppClientSecret"].encode(),
97-
msg,
98-
digestmod=hashlib.sha256,
99-
).digest()
100-
).decode()
101-
102-
cognito_idp_client = boto3.client(
103-
"cognito-idp", region_name=SWITCHBOT_APP_COGNITO_POOL["Region"]
104-
)
126+
105127
try:
106-
auth_response = cognito_idp_client.initiate_auth(
107-
ClientId=SWITCHBOT_APP_COGNITO_POOL["AppClientId"],
108-
AuthFlow="USER_PASSWORD_AUTH",
109-
AuthParameters={
110-
"USERNAME": username,
111-
"PASSWORD": password,
112-
"SECRET_HASH": secret_hash,
128+
auth_result = await SwitchbotLock.api_request(
129+
session,
130+
"account",
131+
"account/api/v1/user/login",
132+
{
133+
"clientId": SWITCHBOT_APP_CLIENT_ID,
134+
"username": username,
135+
"password": password,
136+
"grantType": "password",
137+
"verifyCode": "",
113138
},
114139
)
115-
except cognito_idp_client.exceptions.NotAuthorizedException as err:
116-
raise SwitchbotAuthenticationError(
117-
f"Failed to authenticate: {err}"
118-
) from err
140+
auth_headers = {"authorization": auth_result["access_token"]}
119141
except Exception as err:
120-
raise SwitchbotAuthenticationError(
121-
f"Unexpected error during authentication: {err}"
122-
) from err
142+
raise SwitchbotAuthenticationError(f"Authentication failed: {err}") from err
123143

124-
if (
125-
auth_response is None
126-
or "AuthenticationResult" not in auth_response
127-
or "AccessToken" not in auth_response["AuthenticationResult"]
128-
):
129-
raise SwitchbotAuthenticationError("Unexpected authentication response")
144+
try:
145+
userinfo = await SwitchbotLock.api_request(
146+
session, "account", "account/api/v1/user/userinfo", {}, auth_headers
147+
)
148+
if "botRegion" in userinfo and userinfo["botRegion"] != "":
149+
region = userinfo["botRegion"]
150+
else:
151+
region = "us"
152+
except Exception as err:
153+
raise SwitchbotAccountConnectionError(
154+
f"Failed to retrieve SwitchBot Account user details: {err}"
155+
) from err
130156

131-
access_token = auth_response["AuthenticationResult"]["AccessToken"]
132157
try:
133-
key_response = requests.post(
134-
url=SWITCHBOT_APP_API_BASE_URL + "/developStage/keys/v1/communicate",
135-
headers={"authorization": access_token},
136-
json={
158+
device_info = await SwitchbotLock.api_request(
159+
session,
160+
f"wonderlabs.{region}",
161+
"wonder/keys/v1/communicate",
162+
{
137163
"device_mac": device_mac,
138164
"keyType": "user",
139165
},
140-
timeout=10,
166+
auth_headers,
141167
)
142-
except requests.exceptions.RequestException as err:
168+
169+
return {
170+
"key_id": device_info["communicationKey"]["keyId"],
171+
"encryption_key": device_info["communicationKey"]["key"],
172+
}
173+
except Exception as err:
143174
raise SwitchbotAccountConnectionError(
144175
f"Failed to retrieve encryption key from SwitchBot Account: {err}"
145176
) from err
146-
if key_response.status_code > 299:
147-
raise SwitchbotAuthenticationError(
148-
f"Unexpected status code returned by SwitchBot Account API: {key_response.status_code}"
149-
)
150-
key_response_content = json.loads(key_response.content)
151-
if key_response_content["statusCode"] != 100:
152-
raise SwitchbotAuthenticationError(
153-
f"Unexpected status code returned by SwitchBot API: {key_response_content['statusCode']}"
154-
)
155-
156-
return {
157-
"key_id": key_response_content["body"]["communicationKey"]["keyId"],
158-
"encryption_key": key_response_content["body"]["communicationKey"]["key"],
159-
}
160177

161178
async def lock(self) -> bool:
162179
"""Send lock command."""

0 commit comments

Comments
 (0)