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:
85def create_password(password: str) -> str:
86    hex1 = hashlib.md5(password.encode()).hexdigest()
87    hex2 = hashlib.md5(hex1.encode()).hexdigest()
88    return hashlib.md5(hex2.encode()).hexdigest()
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:
133def check_for_errors_hms(service, response_json: Dict[Any, Any]) -> None:
134    if response_json['message'] is None:
135        service._auth_lib.token.expired = True
136        raise AccessTokenError("Access Token expired, attempting to refresh")
def return_event_for_device( device: wyzeapy.types.Device, events: List[wyzeapy.types.Event]) -> Optional[wyzeapy.types.Event]:
139def return_event_for_device(device: Device, events: List[Event]) -> Optional[Event]:
140    for event in events:
141        if event.device_mac == device.mac:
142            return event
143
144    return None
def create_pid_pair(pid_enum: wyzeapy.types.PropertyIDs, value: str) -> Dict[str, str]:
147def create_pid_pair(pid_enum: PropertyIDs, value: str) -> Dict[str, str]:
148    return {"pid": pid_enum.value, "pvalue": value}