wyzeapy.utils API documentation

# wyzeapy.utils

View Source

      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): View Source

    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): View Source

    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): View Source

    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: View Source

    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: View Source

    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: View Source

     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: View Source

    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: View Source

    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: View Source

    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: View Source

    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]: View Source

    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]: View Source

    147def create_pid_pair(pid_enum: PropertyIDs, value: str) -> Dict[str, str]:
    148    return {"pid": pid_enum.value, "pvalue": value}