Skip to content

Commit 5b634d2

Browse files
dsypniewskibdraco
andauthored
Add retrieve_encryption_key for lock (#170)
Co-authored-by: J. Nick Koston <[email protected]>
1 parent acf2a3e commit 5b634d2

File tree

4 files changed

+93
-76
lines changed

4 files changed

+93
-76
lines changed

scripts/get_encryption_key.py

Lines changed: 6 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,28 @@
11
#!/usr/bin/env python3
2-
import base64
32
import getpass
4-
import hashlib
5-
import hmac
6-
import json
73
import sys
84

9-
import boto3
10-
import requests
11-
12-
# Those values have been obtained from the following files in SwitchBot Android app
13-
# That's how you can verify them yourself
14-
# /assets/switchbot_config.json
15-
# /res/raw/amplifyconfiguration.json
16-
# /res/raw/awsconfiguration.json
17-
SWITCHBOT_INTERNAL_API_BASE_URL = (
18-
"https://l9ren7efdj.execute-api.us-east-1.amazonaws.com"
19-
)
20-
SWITCHBOT_COGNITO_POOL = {
21-
"PoolId": "us-east-1_x1fixo5LC",
22-
"AppClientId": "66r90hdllaj4nnlne4qna0muls",
23-
"AppClientSecret": "1v3v7vfjsiggiupkeuqvsovg084e3msbefpj9rgh611u30uug6t8",
24-
"Region": "us-east-1",
25-
}
5+
from switchbot import SwitchbotLock
266

277

288
def main():
299
if len(sys.argv) < 3:
3010
print(f"Usage: {sys.argv[0]} <device_mac> <username> [<password>]")
3111
exit(1)
3212

33-
device_mac = sys.argv[1].replace(":", "").replace("-", "").upper()
34-
username = sys.argv[2]
3513
if len(sys.argv) == 3:
3614
password = getpass.getpass()
3715
else:
3816
password = sys.argv[3]
3917

40-
msg = bytes(username + SWITCHBOT_COGNITO_POOL["AppClientId"], "utf-8")
41-
secret_hash = base64.b64encode(
42-
hmac.new(
43-
SWITCHBOT_COGNITO_POOL["AppClientSecret"].encode(),
44-
msg,
45-
digestmod=hashlib.sha256,
46-
).digest()
47-
).decode()
48-
49-
cognito_idp_client = boto3.client(
50-
"cognito-idp", region_name=SWITCHBOT_COGNITO_POOL["Region"]
51-
)
52-
auth_response = None
5318
try:
54-
auth_response = cognito_idp_client.initiate_auth(
55-
ClientId=SWITCHBOT_COGNITO_POOL["AppClientId"],
56-
AuthFlow="USER_PASSWORD_AUTH",
57-
AuthParameters={
58-
"USERNAME": username,
59-
"PASSWORD": password,
60-
"SECRET_HASH": secret_hash,
61-
},
62-
)
63-
except cognito_idp_client.exceptions.NotAuthorizedException as e:
64-
print(f"Error: Failed to authenticate - {e}")
65-
exit(1)
66-
except BaseException as e:
67-
print(f"Error: Unexpected error during authentication - {e}")
68-
exit(1)
69-
70-
if (
71-
auth_response is None
72-
or "AuthenticationResult" not in auth_response
73-
or "AccessToken" not in auth_response["AuthenticationResult"]
74-
):
75-
print(f"Error: unexpected authentication result")
76-
exit(1)
77-
78-
access_token = auth_response["AuthenticationResult"]["AccessToken"]
79-
key_response = requests.post(
80-
url=SWITCHBOT_INTERNAL_API_BASE_URL + "/developStage/keys/v1/communicate",
81-
headers={"authorization": access_token},
82-
json={"device_mac": device_mac, "keyType": "user"},
83-
)
84-
key_response_content = json.loads(key_response.content)
85-
if key_response_content["statusCode"] != 100:
86-
print(
87-
"Error: {} ({})".format(
88-
key_response_content["message"], key_response_content["statusCode"]
89-
)
90-
)
19+
result = SwitchbotLock.retrieve_encryption_key(sys.argv[1], sys.argv[2], password)
20+
except RuntimeError as e:
21+
print(e)
9122
exit(1)
9223

93-
print("Key ID: " + key_response_content["body"]["communicationKey"]["keyId"])
94-
print("Encryption key: " + key_response_content["body"]["communicationKey"]["key"])
24+
print("Key ID: " + result["key_id"])
25+
print("Encryption key: " + result["encryption_key"])
9526

9627

9728
if __name__ == "__main__":

setup.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
setup(
44
name="PySwitchbot",
55
packages=["switchbot", "switchbot.devices", "switchbot.adv_parsers"],
6-
install_requires=["async_timeout>=4.0.1", "bleak>=0.17.0", "bleak-retry-connector>=2.9.0", "cryptography>=38.0.3"],
6+
install_requires=[
7+
"async_timeout>=4.0.1",
8+
"bleak>=0.17.0",
9+
"bleak-retry-connector>=2.9.0",
10+
"cryptography>=38.0.3",
11+
"boto3>=1.20.24",
12+
"requests>=2.28.1",
13+
],
714
version="0.33.0",
815
description="A library to communicate with Switchbot",
916
author="Daniel Hjelseth Hoyer",

switchbot/api_config.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Those values have been obtained from the following files in SwitchBot Android app
2+
# That's how you can verify them yourself
3+
# /assets/switchbot_config.json
4+
# /res/raw/amplifyconfiguration.json
5+
# /res/raw/awsconfiguration.json
6+
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+
}

switchbot/devices/lock.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@
22
from __future__ import annotations
33

44
import asyncio
5+
import base64
6+
import hashlib
7+
import hmac
8+
import json
59
import logging
610
from typing import Any
711

12+
import boto3
13+
import requests
814
from bleak.backends.device import BLEDevice
915
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
1016

17+
from ..api_config import SWITCHBOT_APP_API_BASE_URL, SWITCHBOT_APP_COGNITO_POOL
1118
from ..const import LockStatus
1219
from .device import SwitchbotDevice, SwitchbotOperationError
1320

@@ -69,6 +76,65 @@ async def verify_encryption_key(
6976

7077
return lock_info is not None
7178

79+
@staticmethod
80+
def retrieve_encryption_key(device_mac: str, username: str, password: str):
81+
"""Retrieve lock key from internal SwitchBot API."""
82+
device_mac = device_mac.replace(":", "").replace("-", "").upper()
83+
msg = bytes(username + SWITCHBOT_APP_COGNITO_POOL["AppClientId"], "utf-8")
84+
secret_hash = base64.b64encode(
85+
hmac.new(
86+
SWITCHBOT_APP_COGNITO_POOL["AppClientSecret"].encode(),
87+
msg,
88+
digestmod=hashlib.sha256,
89+
).digest()
90+
).decode()
91+
92+
cognito_idp_client = boto3.client(
93+
"cognito-idp", region_name=SWITCHBOT_APP_COGNITO_POOL["Region"]
94+
)
95+
try:
96+
auth_response = cognito_idp_client.initiate_auth(
97+
ClientId=SWITCHBOT_APP_COGNITO_POOL["AppClientId"],
98+
AuthFlow="USER_PASSWORD_AUTH",
99+
AuthParameters={
100+
"USERNAME": username,
101+
"PASSWORD": password,
102+
"SECRET_HASH": secret_hash,
103+
},
104+
)
105+
except cognito_idp_client.exceptions.NotAuthorizedException as err:
106+
raise RuntimeError("Failed to authenticate") from err
107+
except BaseException as err:
108+
raise RuntimeError("Unexpected error during authentication") from err
109+
110+
if (
111+
auth_response is None
112+
or "AuthenticationResult" not in auth_response
113+
or "AccessToken" not in auth_response["AuthenticationResult"]
114+
):
115+
raise RuntimeError("Unexpected authentication response")
116+
117+
access_token = auth_response["AuthenticationResult"]["AccessToken"]
118+
key_response = requests.post(
119+
url=SWITCHBOT_APP_API_BASE_URL + "/developStage/keys/v1/communicate",
120+
headers={"authorization": access_token},
121+
json={
122+
"device_mac": device_mac,
123+
"keyType": "user",
124+
},
125+
timeout=10,
126+
)
127+
key_response_content = json.loads(key_response.content)
128+
if key_response_content["statusCode"] != 100:
129+
raise RuntimeError(
130+
f"Unexpected status code returned by SwitchBot API: {key_response_content['statusCode']}"
131+
)
132+
133+
return {
134+
"key_id": key_response_content["body"]["communicationKey"]["keyId"],
135+
"encryption_key": key_response_content["body"]["communicationKey"]["key"],
136+
}
137+
72138
async def lock(self) -> bool:
73139
"""Send lock command."""
74140
return await self._lock_unlock(

0 commit comments

Comments
 (0)