Skip to content

Commit

Permalink
New encryption of WiFi credentials and set network mode station
Browse files Browse the repository at this point in the history
Required since firmware version 2.4.30.

Fixes: #70
  • Loading branch information
scrool committed Mar 1, 2022
1 parent 6c2ebc4 commit cc46b59
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 6 deletions.
37 changes: 37 additions & 0 deletions tests/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,43 @@ def test_valid(self):
assert expected_cipher == cipher


class TestEncryptWiFiCredentials(unittest.TestCase):
"""Tests for encrypt_wifi_credentials() from `xled.security` module."""

def test_valid_legacy_key(self):
expected_cipher = (
b"e4XXiiUhg4J1FnJEfUQ0BhIji2HGVk1NHU5vGCHfyclF"
b"dX6R8Nd9BSXVKS5nj2FXGU6SWv9CIzztfAvGgTGLUw=="
)

str_password = "Twinkly"
cipher = security.encrypt_wifi_credentials(
str_password, MAC_ADDRESS_TEST, security.SHARED_KEY_WIFI
)
assert expected_cipher == cipher
bytes_password = b"Twinkly"
cipher = security.encrypt_wifi_credentials(
bytes_password, MAC_ADDRESS_TEST, security.SHARED_KEY_WIFI
)
assert expected_cipher == cipher

def test_valid_v2_key(self):
expected_cipher = (
b"R8/Wb0N52RLRU9HAqutebsmJZrNwdMJPOzmXLk4+0cjU"
b"TgXS/J+nZ9icDcTNb5P2Kb6TZP2TCNxpQGtnjetMrg=="
)
str_password = "Twinkly"
cipher = security.encrypt_wifi_credentials(
str_password, MAC_ADDRESS_TEST, security.SHARED_KEY_WIFI_V2
)
assert expected_cipher == cipher
bytes_password = b"Twinkly"
cipher = security.encrypt_wifi_credentials(
bytes_password, MAC_ADDRESS_TEST, security.SHARED_KEY_WIFI_V2
)
assert expected_cipher == cipher


class TestDeriveKey(unittest.TestCase):
"""Tests for derive_key() from `xled.security` module."""

Expand Down
37 changes: 36 additions & 1 deletion xled/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ def set_network_mode_ap(self, password=None):

def set_network_mode_station(self, ssid=None, password=None):
"""
Sets network mode to Station
Sets network mode to Station for firmware up until 2.4.22
The first time you need to provide an ssid and password for
the WIFI to connect to.
Expand Down Expand Up @@ -806,6 +806,41 @@ def set_network_mode_station(self, ssid=None, password=None):
assert all(key in app_response.keys() for key in required_keys)
return app_response

def set_network_mode_station_v2(self, ssid=None, password=None):
"""
Sets network mode to Station since firmware version 2.4.30
The first time you need to provide an ssid and password for
the WIFI to connect to.
:param str ssid: SSID of the access point to connect to
:param str password: password to use
:raises ApplicationError: on application error
:rtype: :class:`~xled.response.ApplicationResponse`
"""
json_payload = {"mode": 1}
if ssid and password:
assert self.hw_address
encpassword = xled.security.encrypt_wifi_credentials(
password, self.hw_address, xled.security.SHARED_KEY_WIFI_V2
)
encssid = xled.security.encrypt_wifi_credentials(
ssid, self.hw_address, xled.security.SHARED_KEY_WIFI_V2
)
json_payload["station"] = {
"dhcp": 1,
"encssid": encssid,
"encpassword": encpassword,
}
else:
assert not ssid and not password
url = urljoin(self.base_url, "network/status")
response = self.session.post(url, json=json_payload)
app_response = ApplicationResponse(response)
required_keys = [u"code"]
assert all(key in app_response.keys() for key in required_keys)
return app_response

def set_playlist(self, entries):
"""
Sets a new playlist
Expand Down
48 changes: 43 additions & 5 deletions xled/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@
#: Default key to encrypt WiFi password
SHARED_KEY_WIFI = b"supersecretkey!!"

#: Default key to encrypt WiFi password and SSID used since firmware v.2.4.25
SHARED_KEY_WIFI_V2 = (
b"\x26\x80\xf5\x87\x9f\xee"
b"\x2c\x75\x11\xaa\x08\x15"
b"\x47\x44\x8e\x04\x99\xcd"
b"\x68\x07\x6e\x09\x32\x62"
b"\x5d\xc4\xde\x7c\x38\x98"
b"\x9e\x88\x80\xee\x2a\xb7"
b"\x33\x67\x8f\xa2\x0d\xcc"
b"\x85\xd8\x94\xcd\x94\x4f"
)

#: Read buffer size for sha1sum
BUFFER_SIZE = 65536

Expand Down Expand Up @@ -118,24 +130,50 @@ def make_challenge_response(challenge_message, mac_address, key=SHARED_KEY_CHALL

def encrypt_wifi_password(password, mac_address, key=SHARED_KEY_WIFI):
"""
Encrypts password
Encrypts WiFi password
This can be used to send password for WiFi in encrypted form over
unencrypted channel. Ideally only device that knows shared secret
key and has defined MAC address should be able to decrypt the
message.
This is backward compatible API which wraps encrypt_wifi_credentials().
Predefined key was used to encrypt only password up until firmware version
2.4.22. Since firmware 2.4.25 a different key is used and also the SSID is
encrypted. While this function still can be used, its name and arguments
might be confusing for readers.
:param str password: password to encrypt
:param str mac_address: MAC address of the remote device in any
format that netaddr.EUI recognizes
:param str key: (optional) shared key that device has to know
:return: Base 64 encoded string of ciphertext of input password
:rtype: str
"""
if not isinstance(password, bytes):
password = bytes(password, "utf-8")
secret_key = derive_key(key, mac_address)
data = password.ljust(64, b"\x00")
return encrypt_wifi_credentials(
credential=password, mac_address=mac_address, shared_key=key
)


def encrypt_wifi_credentials(credential, mac_address, shared_key):
"""
Encrypts WiFi credentials
Derives a secret key out of mac_address and shared_key which is then used to
encrypt the credential. This can be used to send password or SSID for WiFi
in encrypted form over unencrypted channel.
:param str credential: secret in clear text to encrypt
:param str mac_address: MAC address of the remote device in AP mode or from
gestalt call in any format that netaddr.EUI recognizes
:param str shared_key: shared key that device has to know
:return: ciphertext encoded as base 64 string
:rtype: str
"""
if not isinstance(credential, bytes):
credential = bytes(credential, "utf-8")
secret_key = derive_key(shared_key, mac_address)
data = credential.ljust(64, b"\x00")
rc4_encoded = rc4(data, secret_key)
return base64.b64encode(rc4_encoded)

Expand Down

0 comments on commit cc46b59

Please sign in to comment.