diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..361c2a2 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,69 @@ +name: Docker + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +on: + push: + branches: [ "main" ] + # Publish semver tags as releases. + tags: [ 'v*.*.*' ] + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: ghcr.io + # github.repository as / + IMAGE_NAME: ${{ github.repository }} + + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Workaround: https://github.com/docker/build-push-action/issues/461 + - name: Setup Docker buildx + uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f24fa8b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.11-slim + +# Working directory for the application +WORKDIR /usr/src/app + +# Set Entrypoint with hard-coded options +ENTRYPOINT ["python3", "./mz2mqtt.py"] + +COPY requirements.txt /usr/src/app/ + +RUN apt update && apt install -y build-essential \ + && pip3 install --no-cache-dir -r requirements.txt \ + && apt purge -y --auto-remove build-essential && apt clean + +# Copy everything to the working directory (Python files, templates, config) in one go. +COPY . /usr/src/app/ \ No newline at end of file diff --git a/README.md b/README.md index 3238c1f..89bd314 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,65 @@ # mz2mqtt -Send Car Data to mqtt +**Publish all Car Data to MQTT** + +![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg) +![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg) +--- +># !!! WARNING !!! !!! WARNING !!! +>***A too frequent refresh of the data can discharge your 12V starter battery of the car. +So use this Program at your own risk*** +--- + +This Program based on code from bdr99, and it may stop working at any time without warning. + +--- + +Prerequisites: +1. Set up your Car in the app. +2. Create a second Driver for mazda2mqtt. + +## Installation Guide: +Clone the git repository. +Create a virtual environment and install the requirements: +``` +apt install python-virtualenv +cd mz2mqtt + +virtualenv -p python3 ../mz2mqtt.env +source ../mz2mqtt.env/bin/activate + +pip3 install -r requirements.txt +``` +Then copy config_example.yaml to config.yaml an insert your data. +Start mz2mqtt: +``` +cd mz2mqtt +source ../mz2mqtt.env/bin/activate +python mz2mqtt.py +``` + +Or download the Docker Image +``` +docker pull ghcr.io/c64axel/mz2mqtt:main +``` +Start the container with /usr/src/app/config.yaml mapped to the config file +``` +docker run -d --name mz2mqtt --restart unless-stopped -v :/usr/src/app/config.yaml mz2mqtt:main +``` +--- +**MQTT-API** + +To trigger a manual refresh for one car, publish the following via MQTT: +(replace < VIN > with the VIN of the Car) +``` +mz2mqtt/SET//refresh +``` + +--- +### History: + +| Date | Change | +|------------|---------------------------------------------------------------------| +| 26.04.2023 | Initial Version | +| 03.06.2023 | only one refresh at the beginning because risk of battery discharge | +| 08.06.2023 | refresh Data via MQTT | + diff --git a/config_example.yaml b/config_example.yaml new file mode 100644 index 0000000..79be84f --- /dev/null +++ b/config_example.yaml @@ -0,0 +1,14 @@ +mqtt: + host: # your mqtthost + port: 1883 # your mqtt port - default 1883 + user: # your mqtt user or empty when no authentication + password: # your mqtt password + topic: mz2mqtt # your mqtt topic - default mz2mqtt. Leave it default for evcc + clientname: mz2mqtt # your mqtt clientname - default mz2mqtt +mazda: + user: # email address from your Mazda account + password: # password from your Mazda account + region: MME # your Region (MNAO:North America, MME:Europe, MJO:Japan) - default MME +status: + wait: 30 # wait time in minutes getting status - default 30 + refreshwait: 2 # wait time in minutes after refresh data - default 2 diff --git a/mz2mqtt.py b/mz2mqtt.py new file mode 100644 index 0000000..4b55d89 --- /dev/null +++ b/mz2mqtt.py @@ -0,0 +1,140 @@ +import asyncio +import logging +from queue import SimpleQueue + +import paho.mqtt.client as mqtt_client +import mzlib +import yaml + +async def main() -> None: + + cmd_queue = SimpleQueue() + + def create_msg(object, vehicleid, mqtt_topic, indent='/'): + for key in object: + if type(object[key]) == dict: + create_msg(object[key], vehicleid, mqtt_topic, indent + key + '/') + else: + mqttc.publish(mqtt_topic + '/' + str(vehicleid) + indent + key, object[key], 0, True) + return + + async def get_and_publish(vehicle): + logging.info('get and publish data for ' + vehicle['vin']) + vehicle_status = await mazda_client.get_vehicle_status(vehicle['id']) + create_msg(vehicle_status, vehicle['vin'], mqtt_topic) + if vehicle['isElectric']: + vehicle_ev_status = await mazda_client.get_ev_vehicle_status(vehicle['id']) + create_msg(vehicle_ev_status, vehicle['vin'], mqtt_topic) + + def on_connect(client, userdata, flags, rc): + if rc == 0: + logging.info("MQTT connected OK") + else: + logging.error("Bad connection Returned code=", rc) + + def on_message(mosq, obj, msg): + msg.payload = str(msg.payload) + logging.info("received " + msg.topic + " " + msg.payload) + mqtt_cmd = msg.topic.split("/") + if mqtt_cmd[1].upper() == "SET": + cmd_queue.put(mqtt_cmd[2] + ':' + mqtt_cmd[3] + ':' + msg.payload) + + logger = logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + level=logging.INFO) + + # Read Config + with open('config.yaml', 'r') as configfile: + config = yaml.safe_load(configfile) + + # Connect to myMazda + logging.info('Initialize myMazda') + mazda_user = config['mazda']['user'] + mazda_password = config['mazda']['password'] + mazda_region = config['mazda']['region'] or 'MME' + mazda_client = mzlib.Client(mazda_user, mazda_password, mazda_region, use_cached_vehicle_list=True) + + status_wait = (config['status']['wait'] or 30) * 12 + status_refreshwait = (config['status']['refreshwait'] or 2) * 60 + + # Connect to MQTT-Broker + logging.info('Initalize MQTT') + mqtt_broker_address = config['mqtt']['host'] + mqtt_broker_port = config['mqtt']['port'] or 1883 + mqtt_broker_user = config['mqtt']['user'] or None + mqtt_broker_password = config['mqtt']['password'] or None + mqtt_topic = config['mqtt']['topic'] or 'mz2mqtt' + mqtt_clientname = config['mqtt']['clientname'] or 'mz2mqtt' + + mqttc = mqtt_client.Client(mqtt_clientname) + mqttc.enable_logger(logger) + mqttc.on_connect = on_connect + mqttc.on_message = on_message + mqttc.username_pw_set(username=mqtt_broker_user, password=mqtt_broker_password) + mqttc.connect(mqtt_broker_address, mqtt_broker_port, 60) + mqttc.subscribe(mqtt_topic + '/' + 'SET/#', 0) + mqttc.loop_start() + + # Get all Vehicles and publish base + logging.info('Get all vehicles') + try: + vehicles = await mazda_client.get_vehicles() + except Exception: + raise Exception("Failed to get list of vehicles") + + # Publish vehicle data + logging.info('publish all vehicles base data') + for vehicle in vehicles: + create_msg(vehicle,vehicle['vin'], mqtt_topic) + + # refresh all vehicle data at startup + try: + for vehicle in vehicles: + logging.info('refresh data for ' + vehicle['vin']) + await mazda_client.refresh_vehicle_status(vehicle['id']) + except: + logging.error('can not refresh all vehicles data') + await mazda_client.close() + + logging.info('wait ' + str(status_refreshwait) + 's for data after refresh') + await asyncio.sleep(status_refreshwait) + + # Main loop + try: + count = 0 + while True: + # look for new API input + while not cmd_queue.empty(): + r = cmd_queue.get_nowait() + mqtt_cmd = r.split(':') + match mqtt_cmd[1]: + case 'refresh': + found = False + for vehicle in vehicles: + if vehicle['vin'] == mqtt_cmd[0]: + found = True + logging.info('send refresh for ' + vehicle['vin'] + ' and wait' + str(status_refreshwait) + 's') + await mazda_client.refresh_vehicle_status(vehicle['id']) + await asyncio.sleep(status_refreshwait) + await get_and_publish(vehicle) + if not found: + logging.error('VIN ' + mqtt_cmd[0] + ' not found') + case _: + logging.error("invalid command: " + mqtt_cmd[1]) + + # wait time reached and get data + if count == 0: + for vehicle in vehicles: + await get_and_publish(vehicle) + count = status_wait + + count -= 1 + await asyncio.sleep(5) + except: + # Close the session + mqttc.loop_stop() + await mazda_client.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/mz2mqtt.yaml b/mz2mqtt.yaml new file mode 100644 index 0000000..cb5e495 --- /dev/null +++ b/mz2mqtt.yaml @@ -0,0 +1,59 @@ +template: mz2mqtt +products: + - description: + generic: mz2mqtt +group: generic +requirements: + description: + en: myMazda to MQTT. Required MQTT broker configuration and a mz2mqtt installation https://github.com/C64Axel/mz2mqtt. + de: myMazda zu MQTT. Voraussetzung ist ein konfigurierter MQTT Broker und eine mz2mqtt Installation https://github.com/C64Axel/mz2mqtt. +params: + - name: title + - name: vin + required: true + help: + de: Erforderlich + en: Required + - name: capacity + - name: phases + advanced: true + - name: icon + default: car + advanced: true + - name: timeout + default: 720h + advanced: true + - preset: vehicle-identify +render: | + type: custom + {{- if .title }} + title: {{ .title }} + {{- end }} + {{- if .icon }} + icon: {{ .icon }} + {{- end }} + {{- if .capacity }} + capacity: {{ .capacity }} + {{- end }} + {{- if .phases }} + phases: {{ .phases }} + {{- end }} + {{- include "vehicle-identify" . }} + soc: + source: mqtt + topic: mz2mqtt/{{ .vin }}/chargeInfo/batteryLevelPercentage + timeout: {{ .timeout }} + status: + source: combined + plugged: + source: mqtt + topic: mz2mqtt/{{ .vin }}/chargeInfo/pluggedIn + timeout: {{ .timeout }} + charging: + source: mqtt + topic: mz2mqtt/{{ .vin }}/chargeInfo/charging + timeout: {{ .timeout }} + range: + source: mqtt + topic: mz2mqtt/{{ .vin }}/chargeInfo/drivingRangeKm + timeout: {{ .timeout }} diff --git a/mzlib/__init__.py b/mzlib/__init__.py new file mode 100644 index 0000000..8a1b599 --- /dev/null +++ b/mzlib/__init__.py @@ -0,0 +1,9 @@ +from mzlib.client import Client +from mzlib.exceptions import ( + MazdaException, + MazdaAPIEncryptionException, + MazdaAuthenticationException, + MazdaAccountLockedException, + MazdaTokenExpiredException, + MazdaLoginFailedException +) \ No newline at end of file diff --git a/mzlib/client.py b/mzlib/client.py new file mode 100644 index 0000000..6737249 --- /dev/null +++ b/mzlib/client.py @@ -0,0 +1,293 @@ +import datetime +import json + +from mzlib.controller import Controller +from mzlib.exceptions import MazdaConfigException + +class Client: + def __init__(self, email, password, region, websession=None, use_cached_vehicle_list=False): + if email is None or len(email) == 0: + raise MazdaConfigException("Invalid or missing email address") + if password is None or len(password) == 0: + raise MazdaConfigException("Invalid or missing password") + + self.controller = Controller(email, password, region, websession) + + self._cached_state = {} + self._use_cached_vehicle_list = use_cached_vehicle_list + self._cached_vehicle_list = None + + async def validate_credentials(self): + await self.controller.login() + + async def get_vehicles(self): + if self._use_cached_vehicle_list and self._cached_vehicle_list is not None: + return self._cached_vehicle_list + + vec_base_infos_response = await self.controller.get_vec_base_infos() + + vehicles = [] + for i, current_vec_base_info in enumerate(vec_base_infos_response.get("vecBaseInfos")): + current_vehicle_flags = vec_base_infos_response.get("vehicleFlags")[i] + + # Ignore vehicles which are not enrolled in Mazda Connected Services + if current_vehicle_flags.get("vinRegistStatus") != 3: + continue + + other_veh_info = json.loads(current_vec_base_info.get("Vehicle").get("vehicleInformation")) + + nickname = await self.controller.get_nickname(current_vec_base_info.get("vin")) + + vehicle = { + "vin": current_vec_base_info.get("vin"), + "id": current_vec_base_info.get("Vehicle", {}).get("CvInformation", {}).get("internalVin"), + "nickname": nickname, + "carlineCode": other_veh_info.get("OtherInformation", {}).get("carlineCode"), + "carlineName": other_veh_info.get("OtherInformation", {}).get("carlineName"), + "modelYear": other_veh_info.get("OtherInformation", {}).get("modelYear"), + "modelCode": other_veh_info.get("OtherInformation", {}).get("modelCode"), + "modelName": other_veh_info.get("OtherInformation", {}).get("modelName"), + "automaticTransmission": other_veh_info.get("OtherInformation", {}).get("transmissionType") == "A", + "interiorColorCode": other_veh_info.get("OtherInformation", {}).get("interiorColorCode"), + "interiorColorName": other_veh_info.get("OtherInformation", {}).get("interiorColorName"), + "exteriorColorCode": other_veh_info.get("OtherInformation", {}).get("exteriorColorCode"), + "exteriorColorName": other_veh_info.get("OtherInformation", {}).get("exteriorColorName"), + "isElectric": current_vec_base_info.get("econnectType", 0) == 1 + } + + vehicles.append(vehicle) + + if self._use_cached_vehicle_list: + self._cached_vehicle_list = vehicles + return vehicles + + async def get_vehicle_status(self, vehicle_id): + vehicle_status_response = await self.controller.get_vehicle_status(vehicle_id) + + alert_info = vehicle_status_response.get("alertInfos")[0] + remote_info = vehicle_status_response.get("remoteInfos")[0] + + latitude = remote_info.get("PositionInfo", {}).get("Latitude") + if latitude is not None: + latitude = latitude * (-1 if remote_info.get("PositionInfo", {}).get("LatitudeFlag") == 1 else 1) + longitude = remote_info.get("PositionInfo", {}).get("Longitude") + if longitude is not None: + longitude = longitude * (1 if remote_info.get("PositionInfo", {}).get("LongitudeFlag") == 1 else -1) + + vehicle_status = { + "lastUpdatedTimestamp": alert_info.get("OccurrenceDate"), + "latitude": latitude, + "longitude": longitude, + "positionTimestamp": remote_info.get("PositionInfo", {}).get("AcquisitionDatetime"), + "fuelRemainingPercent": remote_info.get("ResidualFuel", {}).get("FuelSegementDActl"), + "fuelDistanceRemainingKm": remote_info.get("ResidualFuel", {}).get("RemDrvDistDActlKm"), + "odometerKm": remote_info.get("DriveInformation", {}).get("OdoDispValue"), + "doors": { + "driverDoorOpen": alert_info.get("Door", {}).get("DrStatDrv") == 1, + "passengerDoorOpen": alert_info.get("Door", {}).get("DrStatPsngr") == 1, + "rearLeftDoorOpen": alert_info.get("Door", {}).get("DrStatRl") == 1, + "rearRightDoorOpen": alert_info.get("Door", {}).get("DrStatRr") == 1, + "trunkOpen": alert_info.get("Door", {}).get("DrStatTrnkLg") == 1, + "hoodOpen": alert_info.get("Door", {}).get("DrStatHood") == 1, + "fuelLidOpen": alert_info.get("Door", {}).get("FuelLidOpenStatus") == 1 + }, + "doorLocks": { + "driverDoorUnlocked": alert_info.get("Door", {}).get("LockLinkSwDrv") == 1, + "passengerDoorUnlocked": alert_info.get("Door", {}).get("LockLinkSwPsngr") == 1, + "rearLeftDoorUnlocked": alert_info.get("Door", {}).get("LockLinkSwRl") == 1, + "rearRightDoorUnlocked": alert_info.get("Door", {}).get("LockLinkSwRr") == 1, + }, + "windows": { + "driverWindowOpen": alert_info.get("Pw", {}).get("PwPosDrv") == 1, + "passengerWindowOpen": alert_info.get("Pw", {}).get("PwPosPsngr") == 1, + "rearLeftWindowOpen": alert_info.get("Pw", {}).get("PwPosRl") == 1, + "rearRightWindowOpen": alert_info.get("Pw", {}).get("PwPosRr") == 1 + }, + "hazardLightsOn": alert_info.get("HazardLamp", {}).get("HazardSw") == 1, + "tirePressure": { + "frontLeftTirePressurePsi": remote_info.get("TPMSInformation", {}).get("FLTPrsDispPsi"), + "frontRightTirePressurePsi": remote_info.get("TPMSInformation", {}).get("FRTPrsDispPsi"), + "rearLeftTirePressurePsi": remote_info.get("TPMSInformation", {}).get("RLTPrsDispPsi"), + "rearRightTirePressurePsi": remote_info.get("TPMSInformation", {}).get("RRTPrsDispPsi") + } + } + + door_lock_status = vehicle_status["doorLocks"] + lock_value = not ( + door_lock_status["driverDoorUnlocked"] + or door_lock_status["passengerDoorUnlocked"] + or door_lock_status["rearLeftDoorUnlocked"] + or door_lock_status["rearRightDoorUnlocked"] + ) + + self.__save_api_value(vehicle_id, "lock_state", lock_value, datetime.datetime.strptime(vehicle_status["lastUpdatedTimestamp"], "%Y%m%d%H%M%S").replace(tzinfo=datetime.timezone.utc)) + + return vehicle_status + + async def get_ev_vehicle_status(self, vehicle_id): + ev_vehicle_status_response = await self.controller.get_ev_vehicle_status(vehicle_id) + + result_data = ev_vehicle_status_response.get("resultData")[0] + vehicle_info = result_data.get("PlusBInformation", {}).get("VehicleInfo", {}) + charge_info = vehicle_info.get("ChargeInfo", {}) + hvac_info = vehicle_info.get("RemoteHvacInfo", {}) + + ev_vehicle_status = { + "lastUpdatedTimestamp": result_data.get("OccurrenceDate"), + "chargeInfo": { + "batteryLevelPercentage": charge_info.get("SmaphSOC"), + "drivingRangeKm": charge_info.get("SmaphRemDrvDistKm"), + "pluggedIn": charge_info.get("ChargerConnectorFitting") == 1, + "charging": charge_info.get("ChargeStatusSub") == 6, + "basicChargeTimeMinutes": charge_info.get("MaxChargeMinuteAC"), + "quickChargeTimeMinutes": charge_info.get("MaxChargeMinuteQBC"), + "batteryHeaterAuto": charge_info.get("CstmzStatBatHeatAutoSW") == 1, + "batteryHeaterOn": charge_info.get("BatteryHeaterON") == 1 + }, + "hvacInfo": { + "hvacOn": hvac_info.get("HVAC") == 1, + "frontDefroster": hvac_info.get("FrontDefroster") == 1, + "rearDefroster": hvac_info.get("RearDefogger") == 1, + "interiorTemperatureCelsius": hvac_info.get("InCarTeDC") + } + } + + self.__save_api_value(vehicle_id, "hvac_mode", ev_vehicle_status["hvacInfo"]["hvacOn"], datetime.datetime.strptime(ev_vehicle_status["lastUpdatedTimestamp"], "%Y%m%d%H%M%S").replace(tzinfo=datetime.timezone.utc)) + + return ev_vehicle_status + + def get_assumed_lock_state(self, vehicle_id): + return self.__get_assumed_value(vehicle_id, "lock_state", datetime.timedelta(seconds=600)) + + def get_assumed_hvac_mode(self, vehicle_id): + return self.__get_assumed_value(vehicle_id, "hvac_mode", datetime.timedelta(seconds=600)) + + def get_assumed_hvac_setting(self, vehicle_id): + return self.__get_assumed_value(vehicle_id, "hvac_setting", datetime.timedelta(seconds=600)) + + async def turn_on_hazard_lights(self, vehicle_id): + await self.controller.light_on(vehicle_id) + + async def turn_off_hazard_lights(self, vehicle_id): + await self.controller.light_off(vehicle_id) + + async def unlock_doors(self, vehicle_id): + self.__save_assumed_value(vehicle_id, "lock_state", False) + + await self.controller.door_unlock(vehicle_id) + + async def lock_doors(self, vehicle_id): + self.__save_assumed_value(vehicle_id, "lock_state", True) + + await self.controller.door_lock(vehicle_id) + + async def start_engine(self, vehicle_id): + await self.controller.engine_start(vehicle_id) + + async def stop_engine(self, vehicle_id): + await self.controller.engine_stop(vehicle_id) + + async def send_poi(self, vehicle_id, latitude, longitude, name): + await self.controller.send_poi(vehicle_id, latitude, longitude, name) + + async def start_charging(self, vehicle_id): + await self.controller.charge_start(vehicle_id) + + async def stop_charging(self, vehicle_id): + await self.controller.charge_stop(vehicle_id) + + async def get_hvac_setting(self, vehicle_id): + response = await self.controller.get_hvac_setting(vehicle_id) + + response_hvac_settings = response.get("hvacSettings", {}) + + hvac_setting = { + "temperature": response_hvac_settings.get("Temperature"), + "temperatureUnit": "C" if response_hvac_settings.get("TemperatureType") == 1 else "F", + "frontDefroster": response_hvac_settings.get("FrontDefroster") == 1, + "rearDefroster": response_hvac_settings.get("RearDefogger") == 1 + } + + self.__save_api_value(vehicle_id, "hvac_setting", hvac_setting) + + return hvac_setting + + async def set_hvac_setting(self, vehicle_id, temperature, temperature_unit, front_defroster, rear_defroster): + self.__save_assumed_value(vehicle_id, "hvac_setting", { + "temperature": temperature, + "temperatureUnit": temperature_unit, + "frontDefroster": front_defroster, + "rearDefroster": rear_defroster + }) + + await self.controller.set_hvac_setting(vehicle_id, temperature, temperature_unit, front_defroster, rear_defroster) + + async def turn_on_hvac(self, vehicle_id): + self.__save_assumed_value(vehicle_id, "hvac_mode", True) + + await self.controller.hvac_on(vehicle_id) + + async def turn_off_hvac(self, vehicle_id): + self.__save_assumed_value(vehicle_id, "hvac_mode", False) + + await self.controller.hvac_off(vehicle_id) + + async def refresh_vehicle_status(self, vehicle_id): + await self.controller.refresh_vehicle_status(vehicle_id) + + async def update_vehicle_nickname(self, vin, new_nickname): + await self.controller.update_nickname(vin, new_nickname) + + async def close(self): + await self.controller.close() + + def __get_assumed_value(self, vehicle_id, key, assumed_state_validity_duration): + cached_state = self.__get_cached_state(vehicle_id) + + assumed_value_key = "assumed_" + key + api_value_key = "api_" + key + assumed_value_timestamp_key = assumed_value_key + "_timestamp" + api_value_timestamp_key = api_value_key + "_timestamp" + + if not assumed_value_key in cached_state and not api_value_key in cached_state: + return None + + if assumed_value_key in cached_state and not api_value_key in cached_state: + return cached_state.get(assumed_value_key) + + if not assumed_value_key in cached_state and api_value_key in cached_state: + return cached_state.get(api_value_key) + + now_timestamp = datetime.datetime.now(datetime.timezone.utc) + + if ( + assumed_value_timestamp_key in cached_state + and api_value_timestamp_key in cached_state + and cached_state.get(assumed_value_timestamp_key) > cached_state.get(api_value_timestamp_key) + and (now_timestamp - cached_state.get(assumed_value_timestamp_key)) < assumed_state_validity_duration + ): + return cached_state.get(assumed_value_key) + + return cached_state.get(api_value_key) + + def __save_assumed_value(self, vehicle_id, key, value, timestamp = None): + cached_state = self.__get_cached_state(vehicle_id) + + timestamp_value = timestamp if timestamp is not None else datetime.datetime.now(datetime.timezone.utc) + + cached_state["assumed_" + key] = value + cached_state["assumed_" + key + "_timestamp"] = timestamp_value + + def __save_api_value(self, vehicle_id, key, value, timestamp = None): + cached_state = self.__get_cached_state(vehicle_id) + + timestamp_value = timestamp if timestamp is not None else datetime.datetime.now(datetime.timezone.utc) + + cached_state["api_" + key] = value + cached_state["api_" + key + "_timestamp"] = timestamp_value + + def __get_cached_state(self, vehicle_id): + if not vehicle_id in self._cached_state: + self._cached_state[vehicle_id] = {} + + return self._cached_state[vehicle_id] diff --git a/mzlib/connection.py b/mzlib/connection.py new file mode 100644 index 0000000..cfde607 --- /dev/null +++ b/mzlib/connection.py @@ -0,0 +1,339 @@ +import aiohttp +import asyncio +import base64 +import hashlib +import json +import logging +import ssl +import time +from urllib.parse import urlencode + +from mzlib.crypto_utils import ( + encrypt_aes128cbc_buffer_to_base64_str, + decrypt_aes128cbc_buffer_to_str, + encrypt_rsaecbpkcs1_padding, + generate_uuid_from_seed, + generate_usher_device_id_from_seed +) + +from mzlib.exceptions import ( + MazdaException, + MazdaConfigException, + MazdaAPIEncryptionException, + MazdaAuthenticationException, + MazdaAccountLockedException, + MazdaTokenExpiredException, + MazdaLoginFailedException, + MazdaRequestInProgressException +) + +from mzlib.sensordata.sensor_data_builder import SensorDataBuilder + +ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +ssl_context.load_default_certs() +ssl_context.set_ciphers("DEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK") + +REGION_CONFIG = { + "MNAO": { + "app_code": "202007270941270111799", + "base_url": "https://0cxo7m58.mazda.com/prod/", + "usher_url": "https://ptznwbh8.mazda.com/appapi/v1/" + }, + "MME": { + "app_code": "202008100250281064816", + "base_url": "https://e9stj7g7.mazda.com/prod/", + "usher_url": "https://rz97suam.mazda.com/appapi/v1/" + }, + "MJO": { + "app_code": "202009170613074283422", + "base_url": "https://wcs9p6wj.mazda.com/prod/", + "usher_url": "https://c5ulfwxr.mazda.com/appapi/v1/" + } +} + +IV = "0102030405060708" +SIGNATURE_MD5 = "C383D8C4D279B78130AD52DC71D95CAA" +APP_PACKAGE_ID = "com.interrait.mymazda" +USER_AGENT_BASE_API = "MyMazda-Android/8.4.2" +USER_AGENT_USHER_API = "MyMazda/8.4.2 (Google Pixel 3a; Android 11)" +APP_OS = "Android" +APP_VERSION = "8.4.2" +USHER_SDK_VERSION = "11.3.0700.001" + +MAX_RETRIES = 4 + +class Connection: + """Main class for handling MyMazda API connection""" + + def __init__(self, email, password, region, websession=None): + self.email = email + self.password = password + + if region in REGION_CONFIG: + region_config = REGION_CONFIG[region] + self.app_code = region_config["app_code"] + self.base_url = region_config["base_url"] + self.usher_url = region_config["usher_url"] + else: + raise MazdaConfigException("Invalid region") + + self.base_api_device_id = generate_uuid_from_seed(email) + self.usher_api_device_id = generate_usher_device_id_from_seed(email) + + self.enc_key = None + self.sign_key = None + + self.access_token = None + self.access_token_expiration_ts = None + + self.sensor_data_builder = SensorDataBuilder() + + if websession is None: + self._session = aiohttp.ClientSession() + else: + self._session = websession + + self.logger = logging.getLogger(__name__) + + def __get_timestamp_str_ms(self): + return str(int(round(time.time() * 1000))) + + def __get_timestamp_str(self): + return str(int(round(time.time()))) + + def __get_decryption_key_from_app_code(self): + val1 = hashlib.md5((self.app_code + APP_PACKAGE_ID).encode()).hexdigest().upper() + val2 = hashlib.md5((val1 + SIGNATURE_MD5).encode()).hexdigest().lower() + return val2[4:20] + + def __get_temporary_sign_key_from_app_code(self): + val1 = hashlib.md5((self.app_code + APP_PACKAGE_ID).encode()).hexdigest().upper() + val2 = hashlib.md5((val1 + SIGNATURE_MD5).encode()).hexdigest().lower() + return val2[20:32] + val2[0:10] + val2[4:6] + + def __get_sign_from_timestamp(self, timestamp): + if timestamp is None or timestamp == "": + return "" + + timestamp_extended = (timestamp + timestamp[6:] + timestamp[3:]).upper() + + temporary_sign_key = self.__get_temporary_sign_key_from_app_code() + + return self.__get_payload_sign(timestamp_extended, temporary_sign_key).upper() + + def __get_sign_from_payload_and_timestamp(self, payload, timestamp): + if timestamp is None or timestamp == "": + return "" + if self.sign_key is None or self.sign_key == "": + raise MazdaException("Missing sign key") + + return self.__get_payload_sign(self.__encrypt_payload_using_key(payload) + timestamp + timestamp[6:] + timestamp[3:], self.sign_key) + + def __get_payload_sign(self, encrypted_payload_and_timestamp, sign_key): + return hashlib.sha256((encrypted_payload_and_timestamp + sign_key).encode()).hexdigest().upper() + + def __encrypt_payload_using_key(self, payload): + if self.enc_key is None or self.enc_key == "": + raise MazdaException("Missing encryption key") + if payload is None or payload == "": + return "" + + return encrypt_aes128cbc_buffer_to_base64_str(payload.encode("utf-8"), self.enc_key, IV) + + def __decrypt_payload_using_app_code(self, payload): + buf = base64.b64decode(payload) + key = self.__get_decryption_key_from_app_code() + decrypted = decrypt_aes128cbc_buffer_to_str(buf, key, IV) + return json.loads(decrypted) + + def __decrypt_payload_using_key(self, payload): + if self.enc_key is None or self.enc_key == "": + raise MazdaException("Missing encryption key") + + buf = base64.b64decode(payload) + decrypted = decrypt_aes128cbc_buffer_to_str(buf, self.enc_key, IV) + return json.loads(decrypted) + + def __encrypt_payload_with_public_key(self, password, public_key): + timestamp = self.__get_timestamp_str() + encryptedBuffer = encrypt_rsaecbpkcs1_padding(password + ":" + timestamp, public_key) + return base64.b64encode(encryptedBuffer).decode("utf-8") + + async def api_request(self, method, uri, query_dict={}, body_dict={}, needs_keys=True, needs_auth=False): + return await self.__api_request_retry(method, uri, query_dict, body_dict, needs_keys, needs_auth, num_retries=0) + + async def __api_request_retry(self, method, uri, query_dict={}, body_dict={}, needs_keys=True, needs_auth=False, num_retries=0): + if num_retries > MAX_RETRIES: + raise MazdaException("Request exceeded max number of retries") + + if needs_keys: + await self.__ensure_keys_present() + if needs_auth: + await self.__ensure_token_is_valid() + + retry_message = (" - attempt #" + str(num_retries + 1)) if (num_retries > 0) else "" + self.logger.debug(f"Sending {method} request to {uri}{retry_message}") + + try: + return await self.__send_api_request(method, uri, query_dict, body_dict, needs_keys, needs_auth) + except (MazdaAPIEncryptionException): + self.logger.info("Server reports request was not encrypted properly. Retrieving new encryption keys.") + await self.__retrieve_keys() + return await self.__api_request_retry(method, uri, query_dict, body_dict, needs_keys, needs_auth, num_retries + 1) + except (MazdaTokenExpiredException): + self.logger.info("Server reports access token was expired. Retrieving new access token.") + await self.login() + return await self.__api_request_retry(method, uri, query_dict, body_dict, needs_keys, needs_auth, num_retries + 1) + except (MazdaLoginFailedException): + self.logger.warning("Login failed for an unknown reason. Trying again.") + await self.login() + return await self.__api_request_retry(method, uri, query_dict, body_dict, needs_keys, needs_auth, num_retries + 1) + except (MazdaRequestInProgressException): + self.logger.info("Request failed because another request was already in progress. Waiting 30 seconds and trying again.") + await asyncio.sleep(30) + return await self.__api_request_retry(method, uri, query_dict, body_dict, needs_keys, needs_auth, num_retries + 1) + + async def __send_api_request(self, method, uri, query_dict={}, body_dict={}, needs_keys=True, needs_auth=False): + timestamp = self.__get_timestamp_str_ms() + + original_query_str = "" + encrypted_query_dict = {} + + if query_dict: + original_query_str = urlencode(query_dict) + encrypted_query_dict["params"] = self.__encrypt_payload_using_key(original_query_str) + + original_body_str = "" + encrypted_body_Str = "" + if body_dict: + original_body_str = json.dumps(body_dict) + encrypted_body_Str = self.__encrypt_payload_using_key(original_body_str) + + headers = { + "device-id": self.base_api_device_id, + "app-code": self.app_code, + "app-os": APP_OS, + "user-agent": USER_AGENT_BASE_API, + "app-version": APP_VERSION, + "app-unique-id": APP_PACKAGE_ID, + "access-token": (self.access_token if needs_auth else ""), + "X-acf-sensor-data": self.sensor_data_builder.generate_sensor_data(), + "req-id": "req_" + timestamp, + "timestamp": timestamp + } + + if "checkVersion" in uri: + headers["sign"] = self.__get_sign_from_timestamp(timestamp) + elif method == "GET": + headers["sign"] = self.__get_sign_from_payload_and_timestamp(original_query_str, timestamp) + elif method == "POST": + headers["sign"] = self.__get_sign_from_payload_and_timestamp(original_body_str, timestamp) + + response = await self._session.request(method, self.base_url + uri, headers=headers, data=encrypted_body_Str, ssl=ssl_context) + + response_json = await response.json() + + if response_json.get("state") == "S": + if "checkVersion" in uri: + return self.__decrypt_payload_using_app_code(response_json["payload"]) + else: + decrypted_payload = self.__decrypt_payload_using_key(response_json["payload"]) + self.logger.debug("Response payload: %s", decrypted_payload) + return decrypted_payload + elif response_json.get("errorCode") == 600001: + raise MazdaAPIEncryptionException("Server rejected encrypted request") + elif response_json.get("errorCode") == 600002: + raise MazdaTokenExpiredException("Token expired") + elif response_json.get("errorCode") == 920000 and response_json.get("extraCode") == "400S01": + raise MazdaRequestInProgressException("Request already in progress, please wait and try again") + elif response_json.get("errorCode") == 920000 and response_json.get("extraCode") == "400S11": + raise MazdaException("The engine can only be remotely started 2 consecutive times. Please drive the vehicle to reset the counter.") + elif "error" in response_json: + raise MazdaException("Request failed: " + response_json["error"]) + else: + raise MazdaException("Request failed for an unknown reason") + + async def __ensure_keys_present(self): + if self.enc_key is None or self.sign_key is None: + await self.__retrieve_keys() + + async def __ensure_token_is_valid(self): + if self.access_token is None or self.access_token_expiration_ts is None: + self.logger.info("No access token present. Logging in.") + elif self.access_token_expiration_ts <= time.time(): + self.logger.info("Access token is expired. Fetching a new one.") + self.access_token = None + self.access_token_expiration_ts = None + + if self.access_token is None or self.access_token_expiration_ts is None or self.access_token_expiration_ts <= time.time(): + await self.login() + + async def __retrieve_keys(self): + self.logger.info("Retrieving encryption keys") + response = await self.api_request("POST", "service/checkVersion", needs_keys=False, needs_auth=False) + self.logger.info("Successfully retrieved encryption keys") + + self.enc_key = response["encKey"] + self.sign_key = response["signKey"] + + async def login(self): + self.logger.info("Logging in as " + self.email) + self.logger.info("Retrieving public key to encrypt password") + encryption_key_response = await self._session.request( + "GET", + self.usher_url + "system/encryptionKey", + params={ + "appId": "MazdaApp", + "locale": "en-US", + "deviceId": self.usher_api_device_id, + "sdkVersion": USHER_SDK_VERSION + }, + headers={ + "User-Agent": USER_AGENT_USHER_API + }, + ssl=ssl_context + ) + + encryption_key_response_json = await encryption_key_response.json() + + public_key = encryption_key_response_json["data"]["publicKey"] + encrypted_password = self.__encrypt_payload_with_public_key(self.password, public_key) + version_prefix = encryption_key_response_json["data"]["versionPrefix"] + + self.logger.info("Sending login request") + login_response = await self._session.request( + "POST", + self.usher_url + "user/login", + headers={ + "User-Agent": USER_AGENT_USHER_API + }, + json={ + "appId": "MazdaApp", + "deviceId": self.usher_api_device_id, + "locale": "en-US", + "password": version_prefix + encrypted_password, + "sdkVersion": USHER_SDK_VERSION, + "userId": self.email, + "userIdType": "email" + }, + ssl=ssl_context + ) + + login_response_json = await login_response.json() + + if login_response_json.get("status") == "INVALID_CREDENTIAL": + self.logger.error("Login failed due to invalid email or password") + raise MazdaAuthenticationException("Invalid email or password") + if login_response_json.get("status") == "USER_LOCKED": + self.logger.error("Login failed to account being locked") + raise MazdaAccountLockedException("Account is locked") + if login_response_json.get("status") != "OK": + self.logger.error("Login failed" + ((": " + login_response_json.get("status", "")) if ("status" in login_response_json) else "")) + raise MazdaLoginFailedException("Login failed") + + self.logger.info("Successfully logged in as " + self.email) + self.access_token = login_response_json["data"]["accessToken"] + self.access_token_expiration_ts = login_response_json["data"]["accessTokenExpirationTs"] + + async def close(self): + await self._session.close() diff --git a/mzlib/controller.py b/mzlib/controller.py new file mode 100644 index 0000000..189e7fc --- /dev/null +++ b/mzlib/controller.py @@ -0,0 +1,305 @@ +import hashlib + +from mzlib.connection import Connection +from mzlib.exceptions import MazdaException + +class Controller: + def __init__(self, email, password, region, websession=None): + self.connection = Connection(email, password, region, websession) + + async def login(self): + await self.connection.login() + + async def get_tac(self): + return await self.connection.api_request("GET", "content/getTac/v4", needs_keys=True, needs_auth=False) + + async def get_language_pkg(self): + postBody = {"platformType": "ANDROID", "region": "MNAO", "version": "2.0.4"} + return await self.connection.api_request("POST", "junction/getLanguagePkg/v4", body_dict=postBody, needs_keys=True, needs_auth=False) + + async def get_vec_base_infos(self): + return await self.connection.api_request("POST", "remoteServices/getVecBaseInfos/v4", body_dict={"internaluserid": "__INTERNAL_ID__"}, needs_keys=True, needs_auth=True) + + async def get_vehicle_status(self, internal_vin): + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin, + "limit": 1, + "offset": 0, + "vecinfotype": "0" + } + response = await self.connection.api_request("POST", "remoteServices/getVehicleStatus/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to get vehicle status") + + return response + + + async def get_ev_vehicle_status(self, internal_vin): + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin, + "limit": 1, + "offset": 0, + "vecinfotype": "0" + } + response = await self.connection.api_request("POST", "remoteServices/getEVVehicleStatus/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to get EV vehicle status") + + return response + + async def get_health_report(self, internal_vin): + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin, + "limit": 1, + "offset": 0 + } + + response = await self.connection.api_request("POST", "remoteServices/getHealthReport/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to get health report") + + return response + + async def door_unlock(self, internal_vin): + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin + } + + response = await self.connection.api_request("POST", "remoteServices/doorUnlock/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to unlock door") + + return response + + async def door_lock(self, internal_vin): + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin + } + + response = await self.connection.api_request("POST", "remoteServices/doorLock/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to lock door") + + return response + + async def light_on(self, internal_vin): + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin + } + + response = await self.connection.api_request("POST", "remoteServices/lightOn/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to turn light on") + + return response + + async def light_off(self, internal_vin): + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin + } + + response = await self.connection.api_request("POST", "remoteServices/lightOff/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to turn light off") + + return response + + async def engine_start(self, internal_vin): + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin + } + + response = await self.connection.api_request("POST", "remoteServices/engineStart/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to start engine") + + return response + + async def engine_stop(self, internal_vin): + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin + } + + response = await self.connection.api_request("POST", "remoteServices/engineStop/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to stop engine") + + return response + + async def get_nickname(self, vin): + if len(vin) != 17: + raise MazdaException("Invalid VIN") + + post_body = { + "internaluserid": "__INTERNAL_ID__", + "vin": vin + } + + response = await self.connection.api_request("POST", "remoteServices/getNickName/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to get vehicle nickname") + + return response["carlineDesc"] + + async def update_nickname(self, vin, new_nickname): + if len(vin) != 17: + raise MazdaException("Invalid VIN") + if len(new_nickname) > 20: + raise MazdaException("Nickname is too long") + + post_body = { + "internaluserid": "__INTERNAL_ID__", + "vin": vin, + "vtitle": new_nickname + } + + response = await self.connection.api_request("POST", "remoteServices/updateNickName/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to update vehicle nickname") + + async def send_poi(self, internal_vin, latitude, longitude, name): + # Calculate a POI ID that is unique to the name and location + poi_id = hashlib.sha256((str(name) + str(latitude) + str(longitude)).encode()).hexdigest()[0:10] + + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin, + "placemarkinfos": [ + { + "Altitude": 0, + "Latitude": abs(latitude), + "LatitudeFlag": 0 if (latitude >= 0) else 1, + "Longitude": abs(longitude), + "LongitudeFlag": 0 if (longitude < 0) else 1, + "Name": name, + "OtherInformation": "{}", + "PoiId": poi_id, + "source": "google" + } + ] + } + + response = await self.connection.api_request("POST", "remoteServices/sendPOI/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to send POI") + + async def charge_start(self, internal_vin): + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin + } + + response = await self.connection.api_request("POST", "remoteServices/chargeStart/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to start charging") + + return response + + async def charge_stop(self, internal_vin): + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin + } + + response = await self.connection.api_request("POST", "remoteServices/chargeStop/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to stop charging") + + return response + + async def get_hvac_setting(self, internal_vin): + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin + } + + response = await self.connection.api_request("POST", "remoteServices/getHVACSetting/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to get HVAC setting") + + return response + + async def set_hvac_setting(self, internal_vin, temperature, temperature_unit, front_defroster, rear_defroster): + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin, + "hvacsettings": { + "FrontDefroster": 1 if front_defroster else 0, + "RearDefogger": 1 if rear_defroster else 0, + "Temperature": temperature, + "TemperatureType": 1 if temperature_unit.lower() == "c" else 2 + } + } + + response = await self.connection.api_request("POST", "remoteServices/updateHVACSetting/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to set HVAC setting") + + return response + + async def hvac_on(self, internal_vin): + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin + } + + response = await self.connection.api_request("POST", "remoteServices/hvacOn/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to turn HVAC on") + + return response + + async def hvac_off(self, internal_vin): + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin + } + + response = await self.connection.api_request("POST", "remoteServices/hvacOff/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to turn HVAC off") + + return response + + async def refresh_vehicle_status(self, internal_vin): + post_body = { + "internaluserid": "__INTERNAL_ID__", + "internalvin": internal_vin + } + + response = await self.connection.api_request("POST", "remoteServices/activeRealTimeVehicleStatus/v4", body_dict=post_body, needs_keys=True, needs_auth=True) + + if response["resultCode"] != "200S00": + raise MazdaException("Failed to refresh vehicle status") + + return response + + async def close(self): + await self.connection.close() \ No newline at end of file diff --git a/mzlib/crypto_utils.py b/mzlib/crypto_utils.py new file mode 100644 index 0000000..dbf82d3 --- /dev/null +++ b/mzlib/crypto_utils.py @@ -0,0 +1,34 @@ +import base64 +import hashlib +from cryptography.hazmat.primitives import padding, serialization +from cryptography.hazmat.primitives.asymmetric import padding as asymmetric_padding +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + + +def encrypt_aes128cbc_buffer_to_base64_str(data, key, iv): + padder = padding.PKCS7(128).padder() + padded_data = padder.update(data) + padder.finalize() + cipher = Cipher(algorithms.AES(key.encode("ascii")), modes.CBC(iv.encode("ascii"))) + encryptor = cipher.encryptor() + encrypted = encryptor.update(padded_data) + encryptor.finalize() + return base64.b64encode(encrypted).decode("utf-8") + +def decrypt_aes128cbc_buffer_to_str(data, key, iv): + cipher = Cipher(algorithms.AES(key.encode("ascii")), modes.CBC(iv.encode("ascii"))) + decryptor = cipher.decryptor() + decrypted = decryptor.update(data) + decryptor.finalize() + unpadder = padding.PKCS7(128).unpadder() + return unpadder.update(decrypted) + unpadder.finalize() + +def encrypt_rsaecbpkcs1_padding(data, public_key): + public_key = serialization.load_der_public_key(base64.b64decode(public_key)) + return public_key.encrypt(data.encode("utf-8"), asymmetric_padding.PKCS1v15()) + +def generate_uuid_from_seed(seed): + hash = hashlib.sha256(seed.encode()).hexdigest().upper() + return hash[0:8] + "-" + hash[8:12] + "-" + hash[12:16] + "-" + hash[16:20] + "-" + hash[20:32] + +def generate_usher_device_id_from_seed(seed): + hash = hashlib.sha256(seed.encode()).hexdigest().upper() + id = int(hash[0:8], 16) + return "ACCT" + str(id) \ No newline at end of file diff --git a/mzlib/exceptions.py b/mzlib/exceptions.py new file mode 100644 index 0000000..8923fd4 --- /dev/null +++ b/mzlib/exceptions.py @@ -0,0 +1,63 @@ +class MazdaConfigException(Exception): + """Raised when Mazda API client is configured incorrectly""" + + def __init__(self, status): + """Initialize exception""" + super(MazdaConfigException, self).__init__(status) + self.status = status + +class MazdaAuthenticationException(Exception): + """Raised when email address or password are invalid during authentication""" + + def __init__(self, status): + """Initialize exception""" + super(MazdaAuthenticationException, self).__init__(status) + self.status = status + +class MazdaAccountLockedException(Exception): + """Raised when account is locked from too many login attempts""" + + def __init__(self, status): + """Initialize exception""" + super(MazdaAccountLockedException, self).__init__(status) + self.status = status + +class MazdaTokenExpiredException(Exception): + """Raised when server reports that the access token has expired""" + + def __init__(self, status): + """Initialize exception""" + super(MazdaTokenExpiredException, self).__init__(status) + self.status = status + +class MazdaAPIEncryptionException(Exception): + """Raised when server reports that the request is not encrypted properly""" + + def __init__(self, status): + """Initialize exception""" + super(MazdaAPIEncryptionException, self).__init__(status) + self.status = status + +class MazdaException(Exception): + """Raised when an unknown error occurs during API interaction""" + + def __init__(self, status): + """Initialize exception""" + super(MazdaException, self).__init__(status) + self.status = status + +class MazdaLoginFailedException(Exception): + """Raised when login fails for an unknown reason""" + + def __init__(self, status): + """Initialize exception""" + super(MazdaLoginFailedException, self).__init__(status) + self.status = status + +class MazdaRequestInProgressException(Exception): + """Raised when a request fails because another request is already in progress""" + + def __init__(self, status): + """Initialize exception""" + super(MazdaRequestInProgressException, self).__init__(status) + self.status = status \ No newline at end of file diff --git a/mzlib/sensordata/__init__.py b/mzlib/sensordata/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mzlib/sensordata/android_builds.py b/mzlib/sensordata/android_builds.py new file mode 100644 index 0000000..0d7cab0 --- /dev/null +++ b/mzlib/sensordata/android_builds.py @@ -0,0 +1,13 @@ +import json + +ANDROID_BUILDS_JSON = '{"Pixel 3":{"codename":"blueline","builds":[{"buildId":"RQ3A.210605.005","version":"11"},{"buildId":"RQ2A.210505.002","version":"11"},{"buildId":"RQ2A.210405.006","version":"11"},{"buildId":"RQ2A.210405.005","version":"11"},{"buildId":"RQ2A.210305.006","version":"11"},{"buildId":"RQ1D.210205.004","version":"11"},{"buildId":"RQ1A.210205.004","version":"11"},{"buildId":"RQ1D.210105.003","version":"11"},{"buildId":"RQ1A.210105.003","version":"11"},{"buildId":"RQ1A.201205.003.A1","version":"11"},{"buildId":"RQ1A.201205.003","version":"11"},{"buildId":"RP1A.201105.002","version":"11"},{"buildId":"RP1A.201005.004","version":"11"},{"buildId":"RP1A.200720.009","version":"11"},{"buildId":"QQ3A.200805.001","version":"10"},{"buildId":"QQ3A.200705.002","version":"10"},{"buildId":"QQ3A.200605.002.A1","version":"10"},{"buildId":"QQ3A.200605.001","version":"10"},{"buildId":"QQ2A.200501.001.B2","version":"10"},{"buildId":"QQ2A.200501.001.A3","version":"10"},{"buildId":"QQ2A.200405.005","version":"10"},{"buildId":"QQ2A.200305.002","version":"10"},{"buildId":"QQ1A.200205.002","version":"10"},{"buildId":"QQ1A.200105.003","version":"10"},{"buildId":"QQ1A.200105.002","version":"10"},{"buildId":"QQ1A.191205.008","version":"10"},{"buildId":"QP1A.191105.003","version":"10"},{"buildId":"QP1A.191005.007","version":"10"},{"buildId":"QP1A.190711.020.C3","version":"10"},{"buildId":"QP1A.190711.020","version":"10"},{"buildId":"QP1A.190711.019","version":"10"},{"buildId":"PQ3A.190801.002","version":"9"},{"buildId":"PQ3A.190705.003","version":"9"},{"buildId":"PQ3A.190605.004.A1","version":"9"},{"buildId":"PQ3A.190605.003","version":"9"},{"buildId":"PQ3A.190505.002","version":"9"},{"buildId":"PQ2A.190405.003","version":"9"},{"buildId":"PQ2A.190305.002","version":"9"},{"buildId":"PQ2A.190205.001","version":"9"},{"buildId":"PQ1A.190105.004","version":"9"},{"buildId":"PQ1A.181205.006.A1","version":"9"},{"buildId":"PQ1A.181205.006","version":"9"},{"buildId":"PQ1A.181105.017.A1","version":"9"},{"buildId":"PD1A.180720.031","version":"9"},{"buildId":"PD1A.180720.030","version":"9"}]},"Pixel 3a":{"codename":"sargo","builds":[{"buildId":"RQ3A.210605.005","version":"11"},{"buildId":"RQ2A.210505.002","version":"11"},{"buildId":"RQ2A.210405.005","version":"11"},{"buildId":"RQ2A.210305.006","version":"11"},{"buildId":"RQ1A.210205.004","version":"11"},{"buildId":"RQ1A.210105.002","version":"11"},{"buildId":"RQ1A.201205.003","version":"11"},{"buildId":"RP1A.201105.002","version":"11"},{"buildId":"RP1A.201005.004","version":"11"},{"buildId":"RP1A.200720.009","version":"11"},{"buildId":"QQ3A.200805.001","version":"10"},{"buildId":"QQ3A.200705.002","version":"10"},{"buildId":"QQ3A.200605.002.A1","version":"10"},{"buildId":"QQ3A.200605.002","version":"10"},{"buildId":"QQ2A.200501.001.B2","version":"10"},{"buildId":"QQ2A.200501.001.A3","version":"10"},{"buildId":"QQ2A.200405.005","version":"10"},{"buildId":"QQ2A.200305.002","version":"10"},{"buildId":"QQ1A.200205.002","version":"10"},{"buildId":"QQ1A.200105.002","version":"10"},{"buildId":"QQ1A.191205.011","version":"10"},{"buildId":"QP1A.191105.003","version":"10"},{"buildId":"QP1A.191005.007","version":"10"},{"buildId":"QP1A.190711.020.C3","version":"10"},{"buildId":"QP1A.190711.020","version":"10"},{"buildId":"QP1A.190711.019","version":"10"},{"buildId":"PQ3B.190801.002","version":"9"},{"buildId":"PQ3B.190705.003","version":"9"},{"buildId":"PQ3B.190605.006","version":"9"},{"buildId":"PD2A.190115.032","version":"9"},{"buildId":"PD2A.190115.029","version":"9"}]},"Pixel 3a XL":{"codename":"bonito","builds":[{"buildId":"RQ3A.210605.005","version":"11"},{"buildId":"RQ2A.210505.002","version":"11"},{"buildId":"RQ2A.210405.005","version":"11"},{"buildId":"RQ2A.210305.006","version":"11"},{"buildId":"RQ1A.210205.004","version":"11"},{"buildId":"RQ1A.210105.002","version":"11"},{"buildId":"RQ1A.201205.003","version":"11"},{"buildId":"RP1A.201105.002","version":"11"},{"buildId":"RP1A.201005.004","version":"11"},{"buildId":"RP1A.200720.009","version":"11"},{"buildId":"QQ3A.200805.001","version":"10"},{"buildId":"QQ3A.200705.002","version":"10"},{"buildId":"QQ3A.200605.002.A1","version":"10"},{"buildId":"QQ3A.200605.002","version":"10"},{"buildId":"QQ2A.200501.001.B2","version":"10"},{"buildId":"QQ2A.200501.001.A3","version":"10"},{"buildId":"QQ2A.200405.005","version":"10"},{"buildId":"QQ2A.200305.002","version":"10"},{"buildId":"QQ1A.200205.002","version":"10"},{"buildId":"QQ1A.200105.002","version":"10"},{"buildId":"QQ1A.191205.011","version":"10"},{"buildId":"QP1A.191105.003","version":"10"},{"buildId":"QP1A.191005.007","version":"10"},{"buildId":"QP1A.190711.020.C3","version":"10"},{"buildId":"QP1A.190711.020","version":"10"},{"buildId":"QP1A.190711.019","version":"10"},{"buildId":"PQ3B.190801.002","version":"9"},{"buildId":"PQ3B.190705.003","version":"9"},{"buildId":"PQ3B.190605.006","version":"9"},{"buildId":"PD2A.190115.032","version":"9"},{"buildId":"PD2A.190115.029","version":"9"}]},"Pixel 3 XL":{"codename":"crosshatch","builds":[{"buildId":"RQ3A.210605.005","version":"11"},{"buildId":"RQ2A.210505.002","version":"11"},{"buildId":"RQ2A.210405.006","version":"11"},{"buildId":"RQ2A.210405.005","version":"11"},{"buildId":"RQ2A.210305.006","version":"11"},{"buildId":"RQ1D.210205.004","version":"11"},{"buildId":"RQ1A.210205.004","version":"11"},{"buildId":"RQ1D.210105.003","version":"11"},{"buildId":"RQ1A.210105.003","version":"11"},{"buildId":"RQ1A.201205.003.A1","version":"11"},{"buildId":"RQ1A.201205.003","version":"11"},{"buildId":"RP1A.201105.002","version":"11"},{"buildId":"RP1A.201005.004","version":"11"},{"buildId":"RP1A.200720.009","version":"11"},{"buildId":"QQ3A.200805.001","version":"10"},{"buildId":"QQ3A.200705.002","version":"10"},{"buildId":"QQ3A.200605.002.A1","version":"10"},{"buildId":"QQ3A.200605.001","version":"10"},{"buildId":"QQ2A.200501.001.B2","version":"10"},{"buildId":"QQ2A.200501.001.A3","version":"10"},{"buildId":"QQ2A.200405.005","version":"10"},{"buildId":"QQ2A.200305.002","version":"10"},{"buildId":"QQ1A.200205.002","version":"10"},{"buildId":"QQ1A.200105.003","version":"10"},{"buildId":"QQ1A.200105.002","version":"10"},{"buildId":"QQ1A.191205.008","version":"10"},{"buildId":"QP1A.191105.003","version":"10"},{"buildId":"QP1A.191005.007","version":"10"},{"buildId":"QP1A.190711.020.C3","version":"10"},{"buildId":"QP1A.190711.020","version":"10"},{"buildId":"QP1A.190711.019","version":"10"},{"buildId":"PQ3A.190801.002","version":"9"},{"buildId":"PQ3A.190705.003","version":"9"},{"buildId":"PQ3A.190605.004.A1","version":"9"},{"buildId":"PQ3A.190605.003","version":"9"},{"buildId":"PQ3A.190505.002","version":"9"},{"buildId":"PQ2A.190405.003","version":"9"},{"buildId":"PQ2A.190305.002","version":"9"},{"buildId":"PQ2A.190205.001","version":"9"},{"buildId":"PQ1A.190105.004","version":"9"},{"buildId":"PQ1A.181205.006.A1","version":"9"},{"buildId":"PQ1A.181205.006","version":"9"},{"buildId":"PQ1A.181105.017.A1","version":"9"},{"buildId":"PD1A.180720.031","version":"9"},{"buildId":"PD1A.180720.030","version":"9"}]},"Pixel 4":{"codename":"flame","builds":[{"buildId":"RQ3A.210605.005","version":"11"},{"buildId":"RQ2A.210505.002","version":"11"},{"buildId":"RQ2A.210405.005","version":"11"},{"buildId":"RQ2A.210305.006","version":"11"},{"buildId":"RQ1A.210205.004","version":"11"},{"buildId":"RQ1A.210105.003","version":"11"},{"buildId":"RQ1A.201205.008.A1","version":"11"},{"buildId":"RQ1A.201205.008","version":"11"},{"buildId":"RP1A.201105.002","version":"11"},{"buildId":"RP1A.201005.004","version":"11"},{"buildId":"RP1A.200720.009","version":"11"},{"buildId":"QQ3A.200805.001","version":"10"},{"buildId":"QQ3A.200705.002","version":"10"},{"buildId":"QQ3A.200605.002.A1","version":"10"},{"buildId":"QQ3A.200605.001","version":"10"},{"buildId":"QQ2A.200501.001.B2","version":"10"},{"buildId":"QQ2A.200501.001.A3","version":"10"},{"buildId":"QQ2A.200405.005","version":"10"},{"buildId":"QQ2A.200305.004.A1","version":"10"},{"buildId":"QQ2A.200305.003","version":"10"},{"buildId":"QQ1D.200205.002","version":"10"},{"buildId":"QQ1C.200205.002","version":"10"},{"buildId":"QQ1B.200205.002","version":"10"},{"buildId":"QQ1D.200105.002","version":"10"},{"buildId":"QQ1C.200105.004","version":"10"},{"buildId":"QQ1B.200105.004","version":"10"},{"buildId":"QQ1C.191205.016.A1","version":"10"},{"buildId":"QQ1B.191205.012.A1","version":"10"},{"buildId":"QQ1B.191205.011","version":"10"},{"buildId":"QD1A.190821.014.C2","version":"10"},{"buildId":"QD1A.190821.014","version":"10"},{"buildId":"QD1A.190821.007.A3","version":"10"},{"buildId":"QD1A.190821.011.C4","version":"10"},{"buildId":"QD1A.190821.011","version":"10"},{"buildId":"QD1A.190821.007","version":"10"}]},"Pixel 4 XL":{"codename":"coral","builds":[{"buildId":"RQ3A.210605.005","version":"11"},{"buildId":"RQ2A.210505.002","version":"11"},{"buildId":"RQ2A.210405.005","version":"11"},{"buildId":"RQ2A.210305.006","version":"11"},{"buildId":"RQ1A.210205.004","version":"11"},{"buildId":"RQ1A.210105.003","version":"11"},{"buildId":"RQ1A.201205.008.A1","version":"11"},{"buildId":"RQ1A.201205.008","version":"11"},{"buildId":"RP1A.201105.002","version":"11"},{"buildId":"RP1A.201005.004","version":"11"},{"buildId":"RP1A.200720.009","version":"11"},{"buildId":"QQ3A.200805.001","version":"10"},{"buildId":"QQ3A.200705.002","version":"10"},{"buildId":"QQ3A.200605.002.A1","version":"10"},{"buildId":"QQ3A.200605.001","version":"10"},{"buildId":"QQ2A.200501.001.B2","version":"10"},{"buildId":"QQ2A.200501.001.A3","version":"10"},{"buildId":"QQ2A.200405.005","version":"10"},{"buildId":"QQ2A.200305.004.A1","version":"10"},{"buildId":"QQ2A.200305.003","version":"10"},{"buildId":"QQ1D.200205.002","version":"10"},{"buildId":"QQ1C.200205.002","version":"10"},{"buildId":"QQ1B.200205.002","version":"10"},{"buildId":"QQ1D.200105.002","version":"10"},{"buildId":"QQ1C.200105.004","version":"10"},{"buildId":"QQ1B.200105.004","version":"10"},{"buildId":"QQ1C.191205.016.A1","version":"10"},{"buildId":"QQ1B.191205.012.A1","version":"10"},{"buildId":"QQ1B.191205.011","version":"10"},{"buildId":"QD1A.190821.014.C2","version":"10"},{"buildId":"QD1A.190821.014","version":"10"},{"buildId":"QD1A.190821.007.A3","version":"10"},{"buildId":"QD1A.190821.011.C4","version":"10"},{"buildId":"QD1A.190821.011","version":"10"},{"buildId":"QD1A.190821.007","version":"10"}]},"Pixel 4a":{"codename":"sunfish","builds":[{"buildId":"RQ3A.210605.005","version":"11"},{"buildId":"RQ2A.210505.002","version":"11"},{"buildId":"RQ2A.210405.005","version":"11"},{"buildId":"RQ2A.210305.007","version":"11"},{"buildId":"RQ2A.210305.006","version":"11"},{"buildId":"RQ1A.210205.004","version":"11"},{"buildId":"RQ1A.210105.002","version":"11"},{"buildId":"RQ1A.201205.008","version":"11"},{"buildId":"RP1A.201105.002","version":"11"},{"buildId":"RP1A.201005.006","version":"11"},{"buildId":"RP1A.200720.011","version":"11"},{"buildId":"RP1A.200720.010","version":"11"},{"buildId":"QD4A.200805.003","version":"10"},{"buildId":"QD4A.200805.001","version":"10"},{"buildId":"QD4A.200317.027","version":"10"},{"buildId":"QD4A.200317.024.A1","version":"10"}]},"Pixel 5":{"codename":"redfin","builds":[{"buildId":"RQ3A.210605.005","version":"11"},{"buildId":"RQ2A.210505.003","version":"11"},{"buildId":"RQ2A.210405.005","version":"11"},{"buildId":"RQ2A.210305.007","version":"11"},{"buildId":"RQ2A.210305.006","version":"11"},{"buildId":"RQ1D.210205.004","version":"11"},{"buildId":"RQ1C.210205.006","version":"11"},{"buildId":"RQ1A.210205.004","version":"11"},{"buildId":"RQ1D.210105.003","version":"11"},{"buildId":"RQ1A.210105.003","version":"11"},{"buildId":"RQ1D.201205.012.A1","version":"11"},{"buildId":"RQ1A.201205.011","version":"11"},{"buildId":"RQ1A.201205.010","version":"11"},{"buildId":"RD1B.201105.010","version":"11"},{"buildId":"RD1A.201105.003.C1","version":"11"},{"buildId":"RD1A.201105.003.B1","version":"11"},{"buildId":"RD1A.201105.003.A1","version":"11"},{"buildId":"RD1A.201105.003","version":"11"},{"buildId":"RD1A.200810.022.A4","version":"11"},{"buildId":"RD1A.200810.021.B3","version":"11"},{"buildId":"RD1A.200810.020.A1","version":"11"},{"buildId":"RD1A.200810.021.A1","version":"11"},{"buildId":"RD1A.200810.020","version":"11"}]},"Pixel 2":{"codename":"walleye","builds":[{"buildId":"RP1A.201005.004.A1","version":"11"},{"buildId":"RP1A.201005.004","version":"11"},{"buildId":"RP1A.200720.009","version":"11"},{"buildId":"QQ3A.200805.001","version":"10"},{"buildId":"QQ3A.200705.002","version":"10"},{"buildId":"QQ3A.200605.002.A1","version":"10"},{"buildId":"QQ3A.200605.001","version":"10"},{"buildId":"QQ2A.200501.001.B3","version":"10"},{"buildId":"QQ2A.200501.001.A3","version":"10"},{"buildId":"QQ2A.200405.005","version":"10"},{"buildId":"QQ2A.200305.002","version":"10"},{"buildId":"QQ1A.200205.002","version":"10"},{"buildId":"QQ1A.200105.002","version":"10"},{"buildId":"QQ1A.191205.008","version":"10"},{"buildId":"QP1A.191105.004","version":"10"},{"buildId":"QP1A.191005.007.A1","version":"10"},{"buildId":"QP1A.190711.020","version":"10"},{"buildId":"QP1A.190711.019","version":"10"},{"buildId":"PQ3A.190801.002","version":"9"},{"buildId":"PQ3A.190705.001","version":"9"},{"buildId":"PQ3A.190605.003","version":"9"},{"buildId":"PQ3A.190505.001","version":"9"},{"buildId":"PQ2A.190405.003","version":"9"},{"buildId":"PQ2A.190305.002","version":"9"},{"buildId":"PQ2A.190205.002","version":"9"},{"buildId":"PQ1A.190105.004","version":"9"},{"buildId":"PQ1A.181205.002","version":"9"},{"buildId":"PQ1A.181105.017.A1","version":"9"},{"buildId":"PPR2.181005.003","version":"9"},{"buildId":"PPR2.180905.005","version":"9"},{"buildId":"PPR1.180610.011","version":"9"},{"buildId":"PPR1.180610.009","version":"9"},{"buildId":"OPM4.171019.021.Q1","version":"8.1.0"},{"buildId":"OPM2.171026.006.G1","version":"8.1.0"},{"buildId":"OPM4.171019.021.E1","version":"8.1.0"},{"buildId":"OPM2.171026.006.C1","version":"8.1.0"},{"buildId":"OPM4.171019.016.B1","version":"8.1.0"},{"buildId":"OPM2.171019.029.B1","version":"8.1.0"},{"buildId":"OPM4.171019.015.A1","version":"8.1.0"},{"buildId":"OPM2.171019.029","version":"8.1.0"},{"buildId":"OPM1.171019.021","version":"8.1.0"},{"buildId":"OPM1.171019.019","version":"8.1.0"},{"buildId":"OPM2.171019.016","version":"8.1.0"},{"buildId":"OPM1.171019.014","version":"8.1.0"},{"buildId":"OPM1.171019.013","version":"8.1.0"},{"buildId":"OPM2.171019.012","version":"8.1.0"},{"buildId":"OPM1.171019.011","version":"8.1.0"},{"buildId":"OPD3.170816.023","version":"8.1.0"},{"buildId":"OPD1.170816.025","version":"8.1.0"},{"buildId":"OPD3.170816.016","version":"8.1.0"},{"buildId":"OPD2.170816.015","version":"8.1.0"},{"buildId":"OPD1.170816.018","version":"8.1.0"},{"buildId":"OPD3.170816.012","version":"8.1.0"},{"buildId":"OPD1.170816.012","version":"8.1.0"},{"buildId":"OPD1.170816.011","version":"8.1.0"},{"buildId":"OPD1.170816.010","version":"8.1.0"}]},"Pixel 2 XL":{"codename":"taimen","builds":[{"buildId":"RP1A.201005.004.A1","version":"11"},{"buildId":"RP1A.201005.004","version":"11"},{"buildId":"RP1A.200720.009","version":"11"},{"buildId":"QQ3A.200805.001","version":"10"},{"buildId":"QQ3A.200705.002","version":"10"},{"buildId":"QQ3A.200605.002.A1","version":"10"},{"buildId":"QQ3A.200605.001","version":"10"},{"buildId":"QQ2A.200501.001.B3","version":"10"},{"buildId":"QQ2A.200501.001.A3","version":"10"},{"buildId":"QQ2A.200405.005","version":"10"},{"buildId":"QQ2A.200305.002","version":"10"},{"buildId":"QQ1A.200205.002","version":"10"},{"buildId":"QQ1A.200105.002","version":"10"},{"buildId":"QQ1A.191205.008","version":"10"},{"buildId":"QP1A.191105.004","version":"10"},{"buildId":"QP1A.191005.007.A1","version":"10"},{"buildId":"QP1A.190711.020","version":"10"},{"buildId":"QP1A.190711.019","version":"10"},{"buildId":"PQ3A.190801.002","version":"9"},{"buildId":"PQ3A.190705.001","version":"9"},{"buildId":"PQ3A.190605.003","version":"9"},{"buildId":"PQ3A.190505.001","version":"9"},{"buildId":"PQ2A.190405.003","version":"9"},{"buildId":"PQ2A.190305.002","version":"9"},{"buildId":"PQ2A.190205.002","version":"9"},{"buildId":"PQ1A.190105.004","version":"9"},{"buildId":"PQ1A.181205.002","version":"9"},{"buildId":"PQ1A.181105.017.A1","version":"9"},{"buildId":"PPR2.181005.003","version":"9"},{"buildId":"PPR2.180905.005","version":"9"},{"buildId":"PPR1.180610.011","version":"9"},{"buildId":"PPR1.180610.009","version":"9"},{"buildId":"OPM4.171019.021.R1","version":"8.1.0"},{"buildId":"OPM2.171026.006.H1","version":"8.1.0"},{"buildId":"OPM4.171019.021.E1","version":"8.1.0"},{"buildId":"OPM2.171026.006.C1","version":"8.1.0"},{"buildId":"OPM4.171019.016.B1","version":"8.1.0"},{"buildId":"OPM2.171019.029.B1","version":"8.1.0"},{"buildId":"OPM4.171019.015.A1","version":"8.1.0"},{"buildId":"OPM2.171019.029","version":"8.1.0"},{"buildId":"OPM1.171019.021","version":"8.1.0"},{"buildId":"OPM1.171019.018","version":"8.1.0"},{"buildId":"OPM1.171019.014","version":"8.1.0"},{"buildId":"OPM1.171019.013","version":"8.1.0"},{"buildId":"OPM2.171019.012","version":"8.1.0"},{"buildId":"OPM1.171019.011","version":"8.1.0"},{"buildId":"OPD3.170816.023","version":"8.1.0"},{"buildId":"OPD1.170816.025","version":"8.1.0"},{"buildId":"OPD3.170816.012","version":"8.1.0"},{"buildId":"OPD1.170816.012","version":"8.1.0"},{"buildId":"OPD1.170816.011","version":"8.1.0"},{"buildId":"OPD1.170816.010","version":"8.1.0"}]},"Pixel XL":{"codename":"marlin","builds":[{"buildId":"QP1A.191005.007.A3","version":"10"},{"buildId":"QP1A.191005.007.A1","version":"10"},{"buildId":"QP1A.190711.020","version":"10"},{"buildId":"QP1A.190711.019","version":"10"},{"buildId":"PQ3A.190801.002","version":"9"},{"buildId":"PQ3A.190705.001","version":"9"},{"buildId":"PQ3A.190605.003","version":"9"},{"buildId":"PQ3A.190505.001","version":"9"},{"buildId":"PQ2A.190405.003","version":"9"},{"buildId":"PQ2A.190305.002","version":"9"},{"buildId":"PQ2A.190205.003","version":"9"},{"buildId":"PQ1A.190105.004","version":"9"},{"buildId":"PQ1A.181205.002.A1","version":"9"},{"buildId":"PPR2.181005.003.A1","version":"9"},{"buildId":"PPR1.181005.003.A1","version":"9"},{"buildId":"PPR2.181005.003","version":"9"},{"buildId":"PPR1.181005.003","version":"9"},{"buildId":"PPR2.180905.006.A1","version":"9"},{"buildId":"PPR2.180905.006","version":"9"},{"buildId":"PPR1.180905.003","version":"9"},{"buildId":"PPR1.180610.010","version":"9"},{"buildId":"PPR1.180610.009","version":"9"},{"buildId":"OPM4.171019.021.P1","version":"8.1.0"},{"buildId":"OPM4.171019.021.D1","version":"8.1.0"},{"buildId":"OPM4.171019.016.B1","version":"8.1.0"},{"buildId":"OPM2.171019.029","version":"8.1.0"},{"buildId":"OPM1.171019.021","version":"8.1.0"},{"buildId":"OPM1.171019.016","version":"8.1.0"},{"buildId":"OPM1.171019.014","version":"8.1.0"},{"buildId":"OPM1.171019.012","version":"8.1.0"},{"buildId":"OPM1.171019.011","version":"8.1.0"},{"buildId":"OPR3.170623.013","version":"8.1.0"},{"buildId":"OPR1.170623.032","version":"8.1.0"},{"buildId":"OPR3.170623.008","version":"8.1.0"},{"buildId":"OPR1.170623.027","version":"8.1.0"},{"buildId":"OPR3.170623.007","version":"8.1.0"},{"buildId":"OPR1.170623.026","version":"8.1.0"},{"buildId":"OPR6.170623.012","version":"8.1.0"},{"buildId":"OPR6.170623.011","version":"8.1.0"},{"buildId":"NZH54D","version":"7.1"},{"buildId":"NKG47S","version":"7.1"},{"buildId":"NHG47Q","version":"7.1"},{"buildId":"NJH47F","version":"7.1"},{"buildId":"NZH54B","version":"7.1"},{"buildId":"NKG47M","version":"7.1"},{"buildId":"NJH47D","version":"7.1"},{"buildId":"NHG47O","version":"7.1"},{"buildId":"NJH47B","version":"7.1"},{"buildId":"NJH34C","version":"7.1"},{"buildId":"NKG47L","version":"7.1"},{"buildId":"NHG47N","version":"7.1"},{"buildId":"NHG47L","version":"7.1"},{"buildId":"N2G47T","version":"7.1"},{"buildId":"N2G47O","version":"7.1"},{"buildId":"NHG47K","version":"7.1"},{"buildId":"N2G47J","version":"7.1"},{"buildId":"N2G47E","version":"7.1"},{"buildId":"NOF27D","version":"7.1"},{"buildId":"NOF27C","version":"7.1"},{"buildId":"NOF27B","version":"7.1"},{"buildId":"NOF26W","version":"7.1"},{"buildId":"NOF26V","version":"7.1"},{"buildId":"NMF26V","version":"7.1"},{"buildId":"NMF26U","version":"7.1"},{"buildId":"NMF26Q","version":"7.1"},{"buildId":"NMF26O","version":"7.1"},{"buildId":"NDE63X","version":"7.1"},{"buildId":"NDE63V","version":"7.1"},{"buildId":"NDE63U","version":"7.1"},{"buildId":"NDE63P","version":"7.1"},{"buildId":"NDE63L","version":"7.1"},{"buildId":"NDE63H","version":"7.1"}]},"Pixel":{"codename":"sailfish","builds":[{"buildId":"QP1A.191005.007.A3","version":"10"},{"buildId":"QP1A.191005.007.A1","version":"10"},{"buildId":"QP1A.190711.020","version":"10"},{"buildId":"QP1A.190711.019","version":"10"},{"buildId":"PQ3A.190801.002","version":"9"},{"buildId":"PQ3A.190705.001","version":"9"},{"buildId":"PQ3A.190605.003","version":"9"},{"buildId":"PQ3A.190505.001","version":"9"},{"buildId":"PQ2A.190405.003","version":"9"},{"buildId":"PQ2A.190305.002","version":"9"},{"buildId":"PQ2A.190205.003","version":"9"},{"buildId":"PQ1A.190105.004","version":"9"},{"buildId":"PQ1A.181205.002.A1","version":"9"},{"buildId":"PPR2.181005.003.A1","version":"9"},{"buildId":"PPR1.181005.003.A1","version":"9"},{"buildId":"PPR2.181005.003","version":"9"},{"buildId":"PPR1.181005.003","version":"9"},{"buildId":"PPR2.180905.006.A1","version":"9"},{"buildId":"PPR2.180905.006","version":"9"},{"buildId":"PPR1.180905.003","version":"9"},{"buildId":"PPR1.180610.010","version":"9"},{"buildId":"PPR1.180610.009","version":"9"},{"buildId":"OPM4.171019.021.P1","version":"8.1.0"},{"buildId":"OPM4.171019.021.D1","version":"8.1.0"},{"buildId":"OPM4.171019.016.B1","version":"8.1.0"},{"buildId":"OPM2.171019.029","version":"8.1.0"},{"buildId":"OPM1.171019.021","version":"8.1.0"},{"buildId":"OPM1.171019.016","version":"8.1.0"},{"buildId":"OPM1.171019.014","version":"8.1.0"},{"buildId":"OPM1.171019.012","version":"8.1.0"},{"buildId":"OPM1.171019.011","version":"8.1.0"},{"buildId":"OPR3.170623.013","version":"8.1.0"},{"buildId":"OPR1.170623.032","version":"8.1.0"},{"buildId":"OPR3.170623.008","version":"8.1.0"},{"buildId":"OPR1.170623.027","version":"8.1.0"},{"buildId":"OPR3.170623.007","version":"8.1.0"},{"buildId":"OPR1.170623.026","version":"8.1.0"},{"buildId":"OPR6.170623.012","version":"8.1.0"},{"buildId":"OPR6.170623.011","version":"8.1.0"},{"buildId":"NZH54D","version":"7.1"},{"buildId":"NKG47S","version":"7.1"},{"buildId":"NHG47Q","version":"7.1"},{"buildId":"NJH47F","version":"7.1"},{"buildId":"NZH54B","version":"7.1"},{"buildId":"NKG47M","version":"7.1"},{"buildId":"NJH47D","version":"7.1"},{"buildId":"NHG47O","version":"7.1"},{"buildId":"NJH47B","version":"7.1"},{"buildId":"NJH34C","version":"7.1"},{"buildId":"NKG47L","version":"7.1"},{"buildId":"NHG47N","version":"7.1"},{"buildId":"NHG47L","version":"7.1"},{"buildId":"N2G47T","version":"7.1"},{"buildId":"N2G47O","version":"7.1"},{"buildId":"NHG47K","version":"7.1"},{"buildId":"N2G47J","version":"7.1"},{"buildId":"N2G47E","version":"7.1"},{"buildId":"NOF27D","version":"7.1"},{"buildId":"NOF27C","version":"7.1"},{"buildId":"NOF27B","version":"7.1"},{"buildId":"NOF26W","version":"7.1"},{"buildId":"NOF26V","version":"7.1"},{"buildId":"NMF26V","version":"7.1"},{"buildId":"NMF26U","version":"7.1"},{"buildId":"NMF26Q","version":"7.1"},{"buildId":"NMF26O","version":"7.1"},{"buildId":"NDE63X","version":"7.1"},{"buildId":"NDE63V","version":"7.1"},{"buildId":"NDE63U","version":"7.1"},{"buildId":"NDE63P","version":"7.1"},{"buildId":"NDE63L","version":"7.1"},{"buildId":"NDE63H","version":"7.1"}]}}' + +class AndroidBuilds: + def __init__(self): + self.builds = None + + def get_builds(self): + if self.builds is None: + self.builds = json.loads(ANDROID_BUILDS_JSON) + + return self.builds \ No newline at end of file diff --git a/mzlib/sensordata/background_event_list.py b/mzlib/sensordata/background_event_list.py new file mode 100644 index 0000000..baa516e --- /dev/null +++ b/mzlib/sensordata/background_event_list.py @@ -0,0 +1,37 @@ +import datetime +import random + +from mzlib.sensordata.sensor_data_util import timestamp_to_millis + +class BackgroundEvent: + def __init__(self, type, timestamp): + self.type = type + self.timestamp = timestamp + + def to_string(self): + return f"{self.type},{self.timestamp}" + +class BackgroundEventList: + def __init__(self): + self.background_events = [] + + def randomize(self, sensor_collection_start_timestamp): + self.background_events = [] + + if random.randrange(0, 10) > 0: + return + + now_timestamp = datetime.datetime.now(datetime.timezone.utc) + time_since_sensor_collection_start = int((now_timestamp - sensor_collection_start_timestamp) / datetime.timedelta(milliseconds=1)) + + if time_since_sensor_collection_start < 10000: + return + + paused_timestamp = timestamp_to_millis(sensor_collection_start_timestamp) + random.randrange(800, 4500) + resumed_timestamp = paused_timestamp + random.randrange(2000, 5000) + + self.background_events.append(BackgroundEvent(2, paused_timestamp)) + self.background_events.append(BackgroundEvent(3, resumed_timestamp)) + + def to_string(self): + return "".join(map(lambda event: event.to_string(), self.background_events)) diff --git a/mzlib/sensordata/key_event_list.py b/mzlib/sensordata/key_event_list.py new file mode 100644 index 0000000..c405f51 --- /dev/null +++ b/mzlib/sensordata/key_event_list.py @@ -0,0 +1,44 @@ +import datetime +import random + +class KeyEvent: + def __init__(self, time, id_char_code_sum, longer_than_before): + self.time = time + self.id_char_code_sum = id_char_code_sum + self.longer_than_before = longer_than_before + + def to_string(self): + return f"2,{self.time},{self.id_char_code_sum}{',1' if self.longer_than_before else ''};" + +class KeyEventList: + def __init__(self): + self.key_events = [] + + def randomize(self, sensor_collection_start_timestamp): + self.key_events = [] + + if random.randrange(0, 20) > 0: + return + + now_timestamp = datetime.datetime.now(datetime.timezone.utc) + time_since_sensor_collection_start = int((now_timestamp - sensor_collection_start_timestamp) / datetime.timedelta(milliseconds=1)) + + if time_since_sensor_collection_start < 10000: + return + + event_count = random.randrange(2, 5) + id_char_code_sum = random.randrange(517, 519) + for i in range(event_count): + time = random.randrange(5000, 8000) if i == 0 else random.randrange(10, 50) + self.key_events.append(KeyEvent(time, id_char_code_sum, random.randrange(0, 2) == 0)) + + def to_string(self): + return "".join(map(lambda event: event.to_string(), self.key_events)) + + def get_sum(self): + sum = 0 + for key_event in self.key_events: + sum += key_event.id_char_code_sum + sum += key_event.time + sum += 2 + return sum \ No newline at end of file diff --git a/mzlib/sensordata/performance_test_results.py b/mzlib/sensordata/performance_test_results.py new file mode 100644 index 0000000..63eba2c --- /dev/null +++ b/mzlib/sensordata/performance_test_results.py @@ -0,0 +1,36 @@ +import random + +class PerformanceTestResults: + def randomize(self): + num_iterations_1 = (random.randrange(350, 600) * 100) - 1 + self.mod_test_result = 16 + self.mod_test_iterations = int(num_iterations_1 / 100) + + num_iterations_2 = (random.randrange(563, 2000) * 100) - 1 + self.float_test_result = 59 + self.float_test_iterations = int(num_iterations_2 / 100) + + num_iterations_3 = (random.randrange(500, 2000) * 100) - 1 + self.sqrt_test_result = num_iterations_3 - 899 + self.sqrt_test_iterations = int(num_iterations_3 / 100) + + num_iterations_4 = (random.randrange(500, 1500) * 100) - 1 + self.trig_test_result = num_iterations_4 + self.trig_test_iterations = int(num_iterations_4 / 100) + + self.loop_test_result = random.randrange(8500, 16000) + + def to_string(self): + values = [ + self.mod_test_result, + self.mod_test_iterations, + self.float_test_result, + self.float_test_iterations, + self.sqrt_test_result, + self.sqrt_test_iterations, + self.trig_test_result, + self.trig_test_iterations, + self.loop_test_result + ] + + return ",".join(map(str, values)) \ No newline at end of file diff --git a/mzlib/sensordata/sensor_data_builder.py b/mzlib/sensordata/sensor_data_builder.py new file mode 100644 index 0000000..6d73fcf --- /dev/null +++ b/mzlib/sensordata/sensor_data_builder.py @@ -0,0 +1,150 @@ +import datetime +from mzlib.sensordata.sensor_data_encryptor import SensorDataEncryptor +import random + +from mzlib.sensordata.background_event_list import BackgroundEventList +from mzlib.sensordata.key_event_list import KeyEventList +from mzlib.sensordata.performance_test_results import PerformanceTestResults +from mzlib.sensordata.sensor_data_util import feistel_cipher, timestamp_to_millis +from mzlib.sensordata.system_info import SystemInfo +from mzlib.sensordata.touch_event_list import TouchEventList + +SDK_VERSION = "2.2.3" + +class SensorDataBuilder: + def __init__(self): + self.sensor_collection_start_timestamp = datetime.datetime.now(datetime.timezone.utc) + self.device_info_time = random.randrange(3, 8) * 1000 + + self.system_info = SystemInfo() + self.system_info.randomize() + + self.touch_event_list = TouchEventList() + self.key_event_list = KeyEventList() + self.background_event_list = BackgroundEventList() + + self.performance_test_results = PerformanceTestResults() + self.performance_test_results.randomize() + + self.sensor_data_encryptor = SensorDataEncryptor() + + def generate_sensor_data(self): + self.touch_event_list.randomize(self.sensor_collection_start_timestamp) + self.key_event_list.randomize(self.sensor_collection_start_timestamp) + self.background_event_list.randomize(self.sensor_collection_start_timestamp) + + random_number = random.randrange(-(2 ** 31), 2 ** 31) + + orientation_event = self.generate_orientation_data_aa() + orientation_event_count = orientation_event.count(";") + motion_event = self.generate_motion_data_aa() + motion_event_count = motion_event.count(";") + + sensor_data = "" + sensor_data += SDK_VERSION + sensor_data += "-1,2,-94,-100," + sensor_data += self.system_info.to_string() + sensor_data += "," + sensor_data += str(self.system_info.get_char_code_sum()) + sensor_data += "," + sensor_data += str(random_number) + sensor_data += "," + sensor_data += str(int(timestamp_to_millis(self.sensor_collection_start_timestamp) / 2)) + sensor_data += "-1,2,-94,-101," + sensor_data += "do_en" + sensor_data += "," + sensor_data += "dm_en" + sensor_data += "," + sensor_data += "t_en" + sensor_data += "-1,2,-94,-102," + sensor_data += self.generate_edited_text() + sensor_data += "-1,2,-94,-108," + sensor_data += self.key_event_list.to_string() + sensor_data += "-1,2,-94,-117," + sensor_data += self.touch_event_list.to_string() + sensor_data += "-1,2,-94,-111," + sensor_data += orientation_event + sensor_data += "-1,2,-94,-109," + sensor_data += motion_event + sensor_data += "-1,2,-94,-144," + sensor_data += self.generate_orientation_data_ac() + sensor_data += "-1,2,-94,-142," + sensor_data += self.generate_orientation_data_ab() + sensor_data += "-1,2,-94,-145," + sensor_data += self.generate_motion_data_ac() + sensor_data += "-1,2,-94,-143," + sensor_data += self.generate_motion_event() + sensor_data += "-1,2,-94,-115," + sensor_data += self.generate_misc_stat(orientation_event_count, motion_event_count) + sensor_data += "-1,2,-94,-106," + sensor_data += self.generate_stored_values_f() + sensor_data += "," + sensor_data += self.generate_stored_values_g() + sensor_data += "-1,2,-94,-120," + sensor_data += self.generate_stored_stack_traces() + sensor_data += "-1,2,-94,-112," + sensor_data += self.performance_test_results.to_string() + sensor_data += "-1,2,-94,-103," + sensor_data += self.background_event_list.to_string() + + encrypted_sensor_data = self.sensor_data_encryptor.encrypt_sensor_data(sensor_data) + return encrypted_sensor_data + + def generate_edited_text(self): + return "" + + def generate_orientation_data_aa(self): + return "" + + def generate_motion_data_aa(self): + return "" + + def generate_orientation_data_ac(self): + return "" + + def generate_orientation_data_ab(self): + return "" + + def generate_motion_data_ac(self): + return "" + + def generate_motion_event(self): + return "" + + def generate_misc_stat(self, orientation_data_count, motion_data_count): + sum_of_text_event_values = self.key_event_list.get_sum() + sum_of_touch_event_timestamps_and_types = self.touch_event_list.get_sum() + orientation_data_b = 0 + motion_data_b = 0 + overall_sum = sum_of_text_event_values + sum_of_touch_event_timestamps_and_types + orientation_data_b + motion_data_b + + now_timestamp = datetime.datetime.now(datetime.timezone.utc) + time_since_sensor_collection_start = int((now_timestamp - self.sensor_collection_start_timestamp) / datetime.timedelta(milliseconds=1)) + + return ",".join([ + str(sum_of_text_event_values), + str(sum_of_touch_event_timestamps_and_types), + str(orientation_data_b), + str(motion_data_b), + str(overall_sum), + str(time_since_sensor_collection_start), + str(len(self.key_event_list.key_events)), + str(len(self.touch_event_list.touch_events)), + str(orientation_data_count), + str(motion_data_count), + str(self.device_info_time), + str(random.randrange(5, 15) * 1000), + "0", + str(feistel_cipher(overall_sum, len(self.key_event_list.key_events) + len(self.touch_event_list.touch_events) + orientation_data_count + motion_data_count, time_since_sensor_collection_start)), + str(timestamp_to_millis(self.sensor_collection_start_timestamp)), + "0" + ]) + + def generate_stored_values_f(self): + return "-1" + + def generate_stored_values_g(self): + return "0" + + def generate_stored_stack_traces(self): + return "" \ No newline at end of file diff --git a/mzlib/sensordata/sensor_data_encryptor.py b/mzlib/sensordata/sensor_data_encryptor.py new file mode 100644 index 0000000..5ee9b95 --- /dev/null +++ b/mzlib/sensordata/sensor_data_encryptor.py @@ -0,0 +1,43 @@ +import base64 +import random +import secrets + +from cryptography.hazmat.primitives import hashes, hmac, padding, serialization +from cryptography.hazmat.primitives.asymmetric import padding as asymmetric_padding +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +RSA_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4sA7vA7N/t1SRBS8tugM2X4bByl0jaCZLqxPOql+qZ3sP4UFayqJTvXjd7eTjMwg1T70PnmPWyh1hfQr4s12oSVphTKAjPiWmEBvcpnPPMjr5fGgv0w6+KM9DLTxcktThPZAGoVcoyM/cTO/YsAMIxlmTzpXBaxddHRwi8S2NvwIDAQAB" + +def to_base64_str(bytes): + return base64.b64encode(bytes).decode("utf-8") + +class SensorDataEncryptor: + def __init__(self): + self.aes_key = secrets.token_bytes(16) + self.aes_iv = secrets.token_bytes(16) + self.hmac_sha256_key = secrets.token_bytes(32) + + public_key = serialization.load_der_public_key(base64.b64decode(RSA_PUBLIC_KEY)) + self.encrypted_aes_key = public_key.encrypt(self.aes_key, asymmetric_padding.PKCS1v15()) + self.encrypted_hmac_sha256_key = public_key.encrypt(self.hmac_sha256_key, asymmetric_padding.PKCS1v15()) + + def encrypt_sensor_data(self, sensor_data): + padder = padding.PKCS7(128).padder() + padded_data = padder.update(sensor_data.encode()) + padder.finalize() + cipher = Cipher(algorithms.AES(self.aes_key), modes.CBC(self.aes_iv)) + encryptor = cipher.encryptor() + encrypted_sensor_data = encryptor.update(padded_data) + encryptor.finalize() + + iv_and_encrypted_sensor_data = self.aes_iv + encrypted_sensor_data + + hmac_obj = hmac.HMAC(self.hmac_sha256_key, hashes.SHA256()) + hmac_obj.update(iv_and_encrypted_sensor_data) + hmac_result = hmac_obj.finalize() + + result = iv_and_encrypted_sensor_data + hmac_result + + aes_timestamp = random.randrange(0, 3) * 1000 + hmac_timestamp = random.randrange(0, 3) * 1000 + base64_timestamp = random.randrange(0, 3) * 1000 + + return f"1,a,{to_base64_str(self.encrypted_aes_key)},{to_base64_str(self.encrypted_hmac_sha256_key)}${to_base64_str(result)}${aes_timestamp},{hmac_timestamp},{base64_timestamp}" \ No newline at end of file diff --git a/mzlib/sensordata/sensor_data_util.py b/mzlib/sensordata/sensor_data_util.py new file mode 100644 index 0000000..a5c2061 --- /dev/null +++ b/mzlib/sensordata/sensor_data_util.py @@ -0,0 +1,46 @@ +def percent_encode(str): + if str is None: + return "" + + result_str = "" + for char in str.encode(): + if char >= 33 and char <= 0x7E and char != 34 and char != 37 and char != 39 and char != 44 and char != 92: + result_str += chr(char) + else: + result_str += "%" + result_str += format(char, "x").upper() + return result_str + +def sum_char_codes(str): + sum = 0 + for char in str.encode(): + if char < 0x80: + sum += char + return sum + +def feistel_cipher(upper_32_bits, lower_32_bits, key): + def to_signed_32(n): + n = n & 0xFFFFFFFF + return n | (-(n & 0x80000000)) + + def iterate(arg1, arg2, arg3): + return arg1 ^ (arg2 >> (32 - arg3) | to_signed_32(arg2 << arg3)) + + upper = to_signed_32(upper_32_bits) + lower = to_signed_32(lower_32_bits) + + data = (lower & 0xFFFFFFFF) | (upper << 32) + + lower2 = to_signed_32(data & 0xFFFFFFFF) + upper2 = to_signed_32((data >> 32) & 0xFFFFFFFF) + + for i in range(16): + v2_1 = upper2 ^ iterate(lower2, key, i) + v8 = lower2 + lower2 = v2_1 + upper2 = v8 + + return (upper2 << 32) | (lower2 & 0xFFFFFFFF) + +def timestamp_to_millis(timestamp): + return int(timestamp.timestamp() * 1000) \ No newline at end of file diff --git a/mzlib/sensordata/system_info.py b/mzlib/sensordata/system_info.py new file mode 100644 index 0000000..31432eb --- /dev/null +++ b/mzlib/sensordata/system_info.py @@ -0,0 +1,101 @@ +import random +import secrets + +from mzlib.sensordata.android_builds import AndroidBuilds +from mzlib.sensordata.sensor_data_util import percent_encode, sum_char_codes + +SCREEN_SIZES = [[1280, 720], [1920, 1080], [2560, 1440]] + +ANDROID_VERSION_TO_SDK_VERSION = { + "11": 30, + "10": 29, + "9": 28, + "8.1.0": 27, + "8.0.0": 26, + "7.1": 25, + "7.0": 24 +} + +class SystemInfo: + def __init__(self): + self.android_builds = AndroidBuilds() + + def randomize(self): + device_model, device = random.choice(list(self.android_builds.get_builds().items())) + codename = device["codename"] + build = random.choice(device["builds"]) + build_version_incremental = random.randrange(1000000, 9999999) + + self.screen_height, self.screen_width = random.choice(SCREEN_SIZES) + self.battery_charging = random.randrange(0, 10) <= 1 + self.battery_level = random.randrange(10, 90) + self.orientation = 1 + self.language = "en" + self.android_version = build["version"] + self.rotation_lock = "1" if random.randrange(0, 10) > 1 else "0" + self.build_model = device_model + self.build_bootloader = str(random.randrange(1000000, 9999999)) + self.build_hardware = codename + self.package_name = "com.interrait.mymazda" + self.android_id = secrets.token_bytes(8).hex() + self.keyboard = 0 + self.adb_enabled = False + self.build_version_codename = "REL" + self.build_version_incremental = build_version_incremental + self.build_version_sdk = ANDROID_VERSION_TO_SDK_VERSION.get(build["version"]) + self.build_manufacturer = "Google" + self.build_product = codename + self.build_tags = "release-keys" + self.build_type = "user" + self.build_user = "android-build" + self.build_display = build["buildId"] + self.build_board = codename + self.build_brand = "google" + self.build_device = codename + self.build_fingerprint = f"google/{codename}/{codename}:{build['version']}/{build['buildId']}/{build_version_incremental}:user/release-keys" + self.build_host = f"abfarm-{random.randrange(10000, 99999)}" + self.build_id = build["buildId"] + + def to_string(self): + return ",".join([ + "-1", + "uaend", + "-1", + str(self.screen_height), + str(self.screen_width), + ("1" if self.battery_charging else "0"), + str(self.battery_level), + str(self.orientation), + percent_encode(self.language), + percent_encode(self.android_version), + self.rotation_lock, + percent_encode(self.build_model), + percent_encode(self.build_bootloader), + percent_encode(self.build_hardware), + "-1", + self.package_name, + "-1", + "-1", + self.android_id, + "-1", + str(self.keyboard), + "1" if self.adb_enabled else "0", + percent_encode(self.build_version_codename), + percent_encode(str(self.build_version_incremental)), + str(self.build_version_sdk), + percent_encode(self.build_manufacturer), + percent_encode(self.build_product), + percent_encode(self.build_tags), + percent_encode(self.build_type), + percent_encode(self.build_user), + percent_encode(self.build_display), + percent_encode(self.build_board), + percent_encode(self.build_brand), + percent_encode(self.build_device), + percent_encode(self.build_fingerprint), + percent_encode(self.build_host), + percent_encode(self.build_id) + ]) + + def get_char_code_sum(self): + return sum_char_codes(self.to_string()) diff --git a/mzlib/sensordata/touch_event_list.py b/mzlib/sensordata/touch_event_list.py new file mode 100644 index 0000000..9e2c2b0 --- /dev/null +++ b/mzlib/sensordata/touch_event_list.py @@ -0,0 +1,76 @@ +import datetime +import random + +class TouchEvent: + def __init__(self, type, time, pointer_count, tool_type): + self.type = type + self.time = time + self.pointer_count = pointer_count + self.tool_type = tool_type + + def to_string(self): + return f"{self.type},{self.time},0,0,{self.pointer_count},1,{self.tool_type},-1;" + +class TouchEventList: + def __init__(self): + self.touch_events = [] + + def randomize(self, sensor_collection_start_timestamp): + self.touch_events = [] + + now_timestamp = datetime.datetime.now(datetime.timezone.utc) + time_since_sensor_collection_start = int((now_timestamp - sensor_collection_start_timestamp) / datetime.timedelta(milliseconds=1)) + + if time_since_sensor_collection_start < 3000: + return + elif time_since_sensor_collection_start >= 3000 and time_since_sensor_collection_start < 5000: + # down event + self.touch_events.append(TouchEvent(2, time_since_sensor_collection_start - random.randrange(1000, 2000), 1, 1)) + + # move events + num_move_events = random.randrange(2, 9) + for i in range(num_move_events): + self.touch_events.append(TouchEvent(1, random.randrange(3, 50), 1, 1)) + + # up event + self.touch_events.append(TouchEvent(3, random.randrange(3, 100), 1, 1)) + elif time_since_sensor_collection_start >= 5000 and time_since_sensor_collection_start < 10000: + for i in range(2): + # down event + self.touch_events.append(TouchEvent(2, random.randrange(100, 1000) + (5000 if i == 1 else 0), 1, 1)) + + # move events + num_move_events = random.randrange(2, 9) + for i in range(num_move_events): + self.touch_events.append(TouchEvent(1, random.randrange(3, 50), 1, 1)) + + # up event + self.touch_events.append(TouchEvent(3, random.randrange(3, 100), 1, 1)) + else: + for i in range(3): + timestamp_offset = 0 + if i == 0: + timestamp_offset = time_since_sensor_collection_start - 9000 + else: + timestamp_offset = random.randrange(2000, 3000) + + # down event + self.touch_events.append(TouchEvent(2, random.randrange(100, 1000) + timestamp_offset, 1, 1)) + + # move events + num_move_events = random.randrange(2, 9) + for i in range(num_move_events): + self.touch_events.append(TouchEvent(1, random.randrange(3, 50), 1, 1)) + + # up event + self.touch_events.append(TouchEvent(3, random.randrange(3, 100), 1, 1)) + + def to_string(self): + return "".join(map(lambda event: event.to_string(), self.touch_events)) + + def get_sum(self): + sum = 0 + for touch_event in self.touch_events: + sum += touch_event.type + sum += touch_event.time + return sum \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2cc4015 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +paho-mqtt~=1.6.1 +PyYAML~=6.0 +cryptography~=43.0.0 +aiohttp~=3.9.5