wyzeapy.utils
1# Copyright (c) 2021. Mulliken, LLC - All Rights Reserved 2# You may use, distribute and modify this code under the terms 3# of the attached license. You should have received a copy of 4# the license with this file. If not, please write to: 5# katie@mulliken.net to receive a copy 6import base64 7import binascii 8import hashlib 9from typing import Dict, Any, List, Optional 10 11from Crypto.Cipher import AES 12 13from .exceptions import ParameterError, AccessTokenError, UnknownApiError 14from .types import ResponseCodes, PropertyIDs, Device, Event 15 16PADDING = bytes.fromhex("05") 17 18 19def pad(plain_text): 20 """ 21 func to pad cleartext to be multiples of 8-byte blocks. 22 If you want to encrypt a text message that is not multiples of 8-byte 23 blocks, the text message must be padded with additional bytes to make the 24 text message to be multiples of 8-byte blocks. 25 """ 26 raw = plain_text.encode("ascii") 27 28 pad_num = AES.block_size - len(raw) % AES.block_size 29 raw += PADDING * pad_num 30 31 return raw 32 33 34def wyze_encrypt(key, text): 35 """ 36 Reimplementation of the Wyze app's encryption mechanism. 37 38 The decompiled code can be found here 👇 39 https://paste.sr.ht/~joshmulliken/e9f67e05c4a774004b226d2ac1f070b6d341cb39 40 """ 41 raw = pad(text) 42 key = key.encode("ascii") 43 iv = key # Wyze uses the secret key for the iv as well 44 cipher = AES.new(key, AES.MODE_CBC, iv) 45 enc = cipher.encrypt(raw) 46 b64_enc = base64.b64encode(enc).decode("ascii") 47 b64_enc = b64_enc.replace("/", r'\/') 48 return b64_enc 49 50 51def wyze_decrypt(key, enc): 52 """ 53 Reimplementation of the Wyze app's decryption mechanism. 54 55 The decompiled code can be found here 👇 56 https://paste.sr.ht/~joshmulliken/e9f67e05c4a774004b226d2ac1f070b6d341cb39 57 """ 58 enc = base64.b64decode(enc) 59 60 key = key.encode('ascii') 61 iv = key 62 cipher = AES.new(key, AES.MODE_CBC, iv) 63 decrypt = cipher.decrypt(enc) 64 65 decrypt_txt = decrypt.decode("ascii") 66 67 return decrypt_txt 68 69 70def wyze_decrypt_cbc(key: str, enc_hex_str: str) -> str: 71 key_hash = hashlib.md5(key.encode("utf-8")).digest() 72 73 iv = b"0123456789ABCDEF" 74 cipher = AES.new(key_hash, AES.MODE_CBC, iv) 75 76 encrypted_bytes = binascii.unhexlify(enc_hex_str) 77 decrypted_bytes = cipher.decrypt(encrypted_bytes) 78 79 # PKCS5Padding 80 padding_length = decrypted_bytes[-1] 81 return decrypted_bytes[:-padding_length].decode() 82 83 84def create_password(password: str) -> str: 85 hex1 = hashlib.md5(password.encode()).hexdigest() 86 hex2 = hashlib.md5(hex1.encode()).hexdigest() 87 return hashlib.md5(hex2.encode()).hexdigest() 88 89 90def check_for_errors_standard(service, response_json: Dict[str, Any]) -> None: 91 response_code = response_json['code'] 92 if response_code != ResponseCodes.SUCCESS.value: 93 if response_code == ResponseCodes.PARAMETER_ERROR.value: 94 raise ParameterError(response_code, response_json['msg']) 95 elif response_code == ResponseCodes.ACCESS_TOKEN_ERROR.value: 96 service._auth_lib.token.expired = True 97 raise AccessTokenError(response_code, "Access Token expired, attempting to refresh") 98 elif response_code == ResponseCodes.DEVICE_OFFLINE.value: 99 return 100 else: 101 raise UnknownApiError(response_code, response_json['msg']) 102 103 104def check_for_errors_lock(service, response_json: Dict[str, Any]) -> None: 105 if response_json['ErrNo'] != 0: 106 if response_json.get('code') == ResponseCodes.PARAMETER_ERROR.value: 107 raise ParameterError(response_json) 108 elif response_json.get('code') == ResponseCodes.ACCESS_TOKEN_ERROR.value: 109 service._auth_lib.token.expired = True 110 raise AccessTokenError("Access Token expired, attempting to refresh") 111 else: 112 raise UnknownApiError(response_json) 113 114 115def check_for_errors_devicemgmt(service, response_json: Dict[Any, Any]) -> None: 116 if response_json['status'] != 200: 117 if "InvalidTokenError>" in response_json['response']['errors'][0]['message']: 118 service._auth_lib.token.expired = True 119 raise AccessTokenError("Access Token expired, attempting to refresh") 120 else: 121 raise UnknownApiError(response_json) 122 123 124def check_for_errors_iot(service, response_json: Dict[Any, Any]) -> None: 125 if response_json['code'] != 1: 126 if str(response_json['code']) == ResponseCodes.ACCESS_TOKEN_ERROR.value: 127 service._auth_lib.token.expired = True 128 raise AccessTokenError("Access Token expired, attempting to refresh") 129 else: 130 raise UnknownApiError(response_json) 131 132def check_for_errors_hms(service, response_json: Dict[Any, Any]) -> None: 133 if response_json['message'] is None: 134 service._auth_lib.token.expired = True 135 raise AccessTokenError("Access Token expired, attempting to refresh") 136 137 138def return_event_for_device(device: Device, events: List[Event]) -> Optional[Event]: 139 for event in events: 140 if event.device_mac == device.mac: 141 return event 142 143 return None 144 145 146def create_pid_pair(pid_enum: PropertyIDs, value: str) -> Dict[str, str]: 147 return {"pid": pid_enum.value, "pvalue": value}
PADDING =
b'\x05'
def
pad(plain_text):
20def pad(plain_text): 21 """ 22 func to pad cleartext to be multiples of 8-byte blocks. 23 If you want to encrypt a text message that is not multiples of 8-byte 24 blocks, the text message must be padded with additional bytes to make the 25 text message to be multiples of 8-byte blocks. 26 """ 27 raw = plain_text.encode("ascii") 28 29 pad_num = AES.block_size - len(raw) % AES.block_size 30 raw += PADDING * pad_num 31 32 return raw
func to pad cleartext to be multiples of 8-byte blocks. If you want to encrypt a text message that is not multiples of 8-byte blocks, the text message must be padded with additional bytes to make the text message to be multiples of 8-byte blocks.
def
wyze_encrypt(key, text):
35def wyze_encrypt(key, text): 36 """ 37 Reimplementation of the Wyze app's encryption mechanism. 38 39 The decompiled code can be found here 👇 40 https://paste.sr.ht/~joshmulliken/e9f67e05c4a774004b226d2ac1f070b6d341cb39 41 """ 42 raw = pad(text) 43 key = key.encode("ascii") 44 iv = key # Wyze uses the secret key for the iv as well 45 cipher = AES.new(key, AES.MODE_CBC, iv) 46 enc = cipher.encrypt(raw) 47 b64_enc = base64.b64encode(enc).decode("ascii") 48 b64_enc = b64_enc.replace("/", r'\/') 49 return b64_enc
Reimplementation of the Wyze app's encryption mechanism.
The decompiled code can be found here 👇 https://paste.sr.ht/~joshmulliken/e9f67e05c4a774004b226d2ac1f070b6d341cb39
def
wyze_decrypt(key, enc):
52def wyze_decrypt(key, enc): 53 """ 54 Reimplementation of the Wyze app's decryption mechanism. 55 56 The decompiled code can be found here 👇 57 https://paste.sr.ht/~joshmulliken/e9f67e05c4a774004b226d2ac1f070b6d341cb39 58 """ 59 enc = base64.b64decode(enc) 60 61 key = key.encode('ascii') 62 iv = key 63 cipher = AES.new(key, AES.MODE_CBC, iv) 64 decrypt = cipher.decrypt(enc) 65 66 decrypt_txt = decrypt.decode("ascii") 67 68 return decrypt_txt
Reimplementation of the Wyze app's decryption mechanism.
The decompiled code can be found here 👇 https://paste.sr.ht/~joshmulliken/e9f67e05c4a774004b226d2ac1f070b6d341cb39
def
wyze_decrypt_cbc(key: str, enc_hex_str: str) -> str:
71def wyze_decrypt_cbc(key: str, enc_hex_str: str) -> str: 72 key_hash = hashlib.md5(key.encode("utf-8")).digest() 73 74 iv = b"0123456789ABCDEF" 75 cipher = AES.new(key_hash, AES.MODE_CBC, iv) 76 77 encrypted_bytes = binascii.unhexlify(enc_hex_str) 78 decrypted_bytes = cipher.decrypt(encrypted_bytes) 79 80 # PKCS5Padding 81 padding_length = decrypted_bytes[-1] 82 return decrypted_bytes[:-padding_length].decode()
def
create_password(password: str) -> str:
def
check_for_errors_standard(service, response_json: Dict[str, Any]) -> None:
91def check_for_errors_standard(service, response_json: Dict[str, Any]) -> None: 92 response_code = response_json['code'] 93 if response_code != ResponseCodes.SUCCESS.value: 94 if response_code == ResponseCodes.PARAMETER_ERROR.value: 95 raise ParameterError(response_code, response_json['msg']) 96 elif response_code == ResponseCodes.ACCESS_TOKEN_ERROR.value: 97 service._auth_lib.token.expired = True 98 raise AccessTokenError(response_code, "Access Token expired, attempting to refresh") 99 elif response_code == ResponseCodes.DEVICE_OFFLINE.value: 100 return 101 else: 102 raise UnknownApiError(response_code, response_json['msg'])
def
check_for_errors_lock(service, response_json: Dict[str, Any]) -> None:
105def check_for_errors_lock(service, response_json: Dict[str, Any]) -> None: 106 if response_json['ErrNo'] != 0: 107 if response_json.get('code') == ResponseCodes.PARAMETER_ERROR.value: 108 raise ParameterError(response_json) 109 elif response_json.get('code') == ResponseCodes.ACCESS_TOKEN_ERROR.value: 110 service._auth_lib.token.expired = True 111 raise AccessTokenError("Access Token expired, attempting to refresh") 112 else: 113 raise UnknownApiError(response_json)
def
check_for_errors_devicemgmt(service, response_json: Dict[Any, Any]) -> None:
116def check_for_errors_devicemgmt(service, response_json: Dict[Any, Any]) -> None: 117 if response_json['status'] != 200: 118 if "InvalidTokenError>" in response_json['response']['errors'][0]['message']: 119 service._auth_lib.token.expired = True 120 raise AccessTokenError("Access Token expired, attempting to refresh") 121 else: 122 raise UnknownApiError(response_json)
def
check_for_errors_iot(service, response_json: Dict[Any, Any]) -> None:
125def check_for_errors_iot(service, response_json: Dict[Any, Any]) -> None: 126 if response_json['code'] != 1: 127 if str(response_json['code']) == ResponseCodes.ACCESS_TOKEN_ERROR.value: 128 service._auth_lib.token.expired = True 129 raise AccessTokenError("Access Token expired, attempting to refresh") 130 else: 131 raise UnknownApiError(response_json)
def
check_for_errors_hms(service, response_json: Dict[Any, Any]) -> None:
def
return_event_for_device( device: wyzeapy.types.Device, events: List[wyzeapy.types.Event]) -> Optional[wyzeapy.types.Event]: