├── .dockerignore ├── .github ├── FUNDING.yml └── workflows │ ├── automerge.yaml │ └── release.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── requirements.txt ├── run.sh ├── run_docker.sh ├── token_extractor.py └── token_extractor.spec /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | * 3 | # Except 4 | !requirements.txt 5 | !token_extractor.py 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: piotrmachowski 2 | custom: ["buycoffee.to/piotrmachowski", "paypal.me/PiMachowski", "revolut.me/314ma"] 3 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yaml: -------------------------------------------------------------------------------- 1 | name: 'Automatically merge master -> dev' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | name: Automatically merge master to dev 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | name: Git checkout 15 | with: 16 | fetch-depth: 0 17 | - name: Merge master -> dev 18 | run: | 19 | git config user.name "GitHub Actions" 20 | git config user.email "PiotrMachowski@users.noreply.github.com" 21 | if (git checkout dev) 22 | then 23 | git merge --ff-only master || git merge --no-commit master 24 | git commit -m "Automatically merge master -> dev" || echo "No commit needed" 25 | git push origin dev 26 | else 27 | echo "No dev branch" 28 | fi -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | name: Prepare release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Download repo 13 | uses: actions/checkout@v1 14 | 15 | - name: Create token_extractor.exe 16 | uses: JackMcKew/pyinstaller-action-windows@main 17 | with: 18 | path: . 19 | 20 | - name: Create token_extractor.zip 21 | run: | 22 | cd /home/runner/work/Xiaomi-cloud-tokens-extractor/Xiaomi-cloud-tokens-extractor 23 | mkdir -p token_extractor 24 | cp token_extractor.py token_extractor/ 25 | cp requirements.txt token_extractor/ 26 | zip token_extractor.zip -r token_extractor 27 | rm -rf token_extractor 28 | 29 | - name: Create token_extractor_docker.zip 30 | run: | 31 | cd /home/runner/work/Xiaomi-cloud-tokens-extractor/Xiaomi-cloud-tokens-extractor 32 | mkdir -p token_extractor_docker 33 | cp token_extractor.py token_extractor_docker/ 34 | cp requirements.txt token_extractor_docker/ 35 | cp .dockerignore token_extractor_docker/ 36 | cp Dockerfile token_extractor_docker/ 37 | zip token_extractor_docker.zip -r token_extractor_docker 38 | rm -rf token_extractor_docker 39 | 40 | - name: Upload token_extractor.exe to release 41 | uses: svenstaro/upload-release-action@v1-release 42 | with: 43 | repo_token: ${{ secrets.GITHUB_TOKEN }} 44 | file: /home/runner/work/Xiaomi-cloud-tokens-extractor/Xiaomi-cloud-tokens-extractor/dist/windows/token_extractor.exe 45 | asset_name: token_extractor.exe 46 | tag: ${{ github.ref }} 47 | overwrite: true 48 | 49 | - name: Upload token_extractor.zip to release 50 | uses: svenstaro/upload-release-action@v1-release 51 | with: 52 | repo_token: ${{ secrets.GITHUB_TOKEN }} 53 | file: /home/runner/work/Xiaomi-cloud-tokens-extractor/Xiaomi-cloud-tokens-extractor/token_extractor.zip 54 | asset_name: token_extractor.zip 55 | tag: ${{ github.ref }} 56 | overwrite: true 57 | 58 | - name: Upload token_extractor_docker.zip to release 59 | uses: svenstaro/upload-release-action@v1-release 60 | with: 61 | repo_token: ${{ secrets.GITHUB_TOKEN }} 62 | file: /home/runner/work/Xiaomi-cloud-tokens-extractor/Xiaomi-cloud-tokens-extractor/token_extractor_docker.zip 63 | asset_name: token_extractor_docker.zip 64 | tag: ${{ github.ref }} 65 | overwrite: true 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 102 | __pypackages__/ 103 | 104 | # Celery stuff 105 | celerybeat-schedule 106 | celerybeat.pid 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ 137 | 138 | # pytype static type analyzer 139 | .pytype/ 140 | 141 | # Cython debug symbols 142 | cython_debug/ 143 | 144 | # End of https://www.toptal.com/developers/gitignore/api/python 145 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY requirements.txt ./ 6 | RUN apk add build-base 7 | RUN pip3 install --no-cache-dir -r requirements.txt 8 | 9 | COPY token_extractor.py ./ 10 | 11 | CMD [ "python", "./token_extractor.py" ] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Piotr Machowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub Latest Release][releases_shield]][latest_release] 2 | [![GitHub All Releases][downloads_total_shield]][releases] 3 | [![Ko-Fi][ko_fi_shield]][ko_fi] 4 | [![buycoffee.to][buycoffee_to_shield]][buycoffee_to] 5 | [![PayPal.Me][paypal_me_shield]][paypal_me] 6 | [![Revolut.Me][revolut_me_shield]][revolut_me] 7 | 8 | 9 | [latest_release]: https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor/releases/latest 10 | [releases_shield]: https://img.shields.io/github/release/PiotrMachowski/Xiaomi-cloud-tokens-extractor.svg?style=popout 11 | 12 | [releases]: https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor/releases 13 | [downloads_total_shield]: https://img.shields.io/github/downloads/PiotrMachowski/Xiaomi-cloud-tokens-extractor/total 14 | 15 | 16 | # Xiaomi Cloud Tokens Extractor 17 | 18 | This tool/script retrieves tokens for all devices connected to Xiaomi cloud and encryption keys for BLE devices. 19 | 20 | You will need to provide Xiaomi Home credentials (_not ones from Roborock app_): 21 | - username (e-mail or Xiaomi Cloud account ID) 22 | - password 23 | - Xiaomi's server region (`cn` - China, `de` - Germany etc.). Leave empty to check all available 24 | 25 | In return all of your devices connected to account will be listed, together with their name and IP address. 26 | 27 | ## Windows 28 | Download and run [token_extractor.exe](https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor/releases/latest/download/token_extractor.exe). 29 | 30 | ## Linux & Home Assistant (in [SSH & Web Terminal](https://github.com/hassio-addons/addon-ssh)) 31 | 32 | Execute following command: 33 | ```bash 34 | bash <(curl -L https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor/raw/master/run.sh) 35 | ``` 36 | 37 | > If installation fails try Docker version 38 | 39 | ## Docker & Home Assistant (in [SSH & Web Terminal](https://github.com/hassio-addons/addon-ssh)) 40 | 41 | Execute following command: 42 | ```bash 43 | bash <(curl -L https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor/raw/master/run_docker.sh) 44 | ``` 45 | 46 | > To run this command in HA you have to disable `protected mode` in addon's settings and restart it 47 | 48 | ## Manual run in python 49 | 50 | Download and unpack archive: 51 | ```bash 52 | wget https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor/releases/latest/download/token_extractor.zip 53 | unzip token_extractor.zip 54 | cd token_extractor 55 | ``` 56 | 57 | Install dependencies and run script: 58 | ```bash 59 | pip3 install -r requirements.txt 60 | python3 token_extractor.py 61 | ``` 62 | 63 | ## Troubleshooting 64 | 65 | If you have problems with using this tool try following solutions: 66 | - Make yourself sure that you provide correct credentials (_e.g. not ones from Roborock app!_) 67 | - Remove Cloudflare DNS 68 | - Disable network ad blockers (AdGuard, PiHole, etc.) and restrictions (UniFi Country Restriction etc.) 69 | - Open 2FA link on the same device that runs Tokens Extractor 70 | 71 | ## Home Assistant additional tools 72 | 73 | * [Map extractor](https://github.com/PiotrMachowski/Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor) - live map for Xiaomi Vacuums 74 | * [Map card](https://github.com/PiotrMachowski/lovelace-xiaomi-vacuum-map-card) - manual vacuum control from a Lovelace card 75 | 76 | 77 | 78 | 79 | ## Support 80 | 81 | If you want to support my work with a donation you can use one of the following platforms: 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 96 | 99 | 103 | 104 | 105 | 106 | 110 | 113 | 114 | 115 | 116 | 117 | 120 | 123 | 126 | 127 | 128 | 129 | 133 | 136 | 139 | 140 |
PlatformPayment methodsLinkComment
Ko-fi 93 |
  • PayPal
  • 94 |
  • Credit card
  • 95 |
    97 | Buy Me a Coffee at ko-fi.com 98 | 100 |
  • No fees
  • 101 |
  • Single or monthly payment
  • 102 |
    buycoffee.to 107 |
  • BLIK
  • 108 |
  • Bank transfer
  • 109 |
    111 | Postaw mi kawę na buycoffee.to 112 |
    PayPal 118 |
  • PayPal
  • 119 |
    121 | PayPal Logo 122 | 124 |
  • No fees
  • 125 |
    Revolut 130 |
  • Revolut
  • 131 |
  • Credit Card
  • 132 |
    134 | Revolut 135 | 137 |
  • No fees
  • 138 |
    141 | 142 | ### Powered by 143 | [![PyCharm logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSourceSupport) 144 | 145 | 146 | [ko_fi_shield]: https://img.shields.io/static/v1.svg?label=%20&message=Ko-Fi&color=F16061&logo=ko-fi&logoColor=white 147 | 148 | [ko_fi]: https://ko-fi.com/piotrmachowski 149 | 150 | [buycoffee_to_shield]: https://shields.io/badge/buycoffee.to-white?style=flat&labelColor=white&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABhmlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw1AUhU9TpaIVh1YQcchQnayIijhKFYtgobQVWnUweemP0KQhSXFxFFwLDv4sVh1cnHV1cBUEwR8QVxcnRRcp8b6k0CLGC4/3cd49h/fuA4R6malmxzigapaRisfEbG5FDLzChxB6MIZ+iZl6Ir2QgWd93VM31V2UZ3n3/Vm9St5kgE8knmW6YRGvE09vWjrnfeIwK0kK8TnxqEEXJH7kuuzyG+eiwwLPDBuZ1BxxmFgstrHcxqxkqMRTxBFF1ShfyLqscN7irJarrHlP/sJgXltOc53WEOJYRAJJiJBRxQbKsBClXSPFRIrOYx7+QcefJJdMrg0wcsyjAhWS4wf/g9+zNQuTE25SMAZ0vtj2xzAQ2AUaNdv+PrbtxgngfwautJa/UgdmPkmvtbTIEdC3DVxctzR5D7jcAQaedMmQHMlPSygUgPcz+qYcELoFulfduTXPcfoAZGhWSzfAwSEwUqTsNY93d7XP7d+e5vx+AIahcq//o+yoAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5wETCy4vFNqLzwAAAVpJREFUOMvd0rFLVXEYxvHPOedKJnKJhrDLuUFREULE7YDCMYj+AydpsCWiaKu29hZxiP4Al4aWwC1EdFI4Q3hqEmkIBI8ZChWXKNLLvS0/Qcza84V3enm/7/s878t/HxGkeTaIGziP+EB918nawu7Dq1d0e1+2J2bepnk2jFEUVVF+qKV51o9neBCaugfge70keoxxUbSWjrQ+4SUyzKZ5NlnDZdzGG7w4DIh+dtZEFntDA98l8S0MYwctNGrYz9WqKJePFLq80g5Sr+EHlnATp+NA+4qLaZ7FfzMrzbMBjGEdq8GrJMZnvAvFC/8wfAwjWMQ8XmMzaW9sdevNRgd3MFhvNpbaG1u/Dk2/hOc4gadVUa7Um425qii/7Z+xH9O4jwW8Cqv24Tru4hyeVEU588cfBMgpPMI9nMFe0BkFzVOYrYqycyQgQJLwTC2cDZCPeF8V5Y7jGb8BUpRicy7OU5MAAAAASUVORK5CYII= 151 | 152 | [buycoffee_to]: https://buycoffee.to/piotrmachowski 153 | 154 | [buy_me_a_coffee_shield]: https://img.shields.io/static/v1.svg?label=%20&message=Buy%20me%20a%20coffee&color=6f4e37&logo=buy%20me%20a%20coffee&logoColor=white 155 | 156 | [buy_me_a_coffee]: https://www.buymeacoffee.com/PiotrMachowski 157 | 158 | [paypal_me_shield]: https://img.shields.io/static/v1.svg?label=%20&message=PayPal.Me&logo=paypal 159 | 160 | [paypal_me]: https://paypal.me/PiMachowski 161 | 162 | [revolut_me_shield]: https://img.shields.io/static/v1.svg?label=%20&message=Revolut&logo=revolut 163 | 164 | [revolut_me]: https://revolut.me/314ma 165 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | pycryptodome 3 | charset-normalizer 4 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit # fail on first error 3 | set -o nounset # fail on undef var 4 | set -o pipefail # fail on first error in pipe 5 | 6 | curl --silent --fail --show-error --location --remote-name --remote-header-name\ 7 | https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor/releases/latest/download/token_extractor.zip 8 | unzip token_extractor.zip 9 | cd token_extractor 10 | pip3 install -r requirements.txt 11 | python3 token_extractor.py 12 | cd .. 13 | rm -rf token_extractor token_extractor.zip 14 | -------------------------------------------------------------------------------- /run_docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit # fail on first error 3 | set -o nounset # fail on undef var 4 | set -o pipefail # fail on first error in pipe 5 | 6 | curl --silent --fail --show-error --location --remote-name --remote-header-name\ 7 | https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor/releases/latest/download/token_extractor_docker.zip 8 | unzip token_extractor_docker.zip 9 | cd token_extractor_docker 10 | docker_image=$(docker build -q .) 11 | docker run --rm -it $docker_image 12 | docker rmi $docker_image 13 | cd .. 14 | rm -rf token_extractor_docker token_extractor_docker.zip 15 | -------------------------------------------------------------------------------- /token_extractor.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | import hmac 4 | import json 5 | import os 6 | import random 7 | import time 8 | from getpass import getpass 9 | from sys import platform 10 | 11 | import requests 12 | from Crypto.Cipher import ARC4 13 | 14 | if platform != "win32": 15 | import readline 16 | 17 | 18 | class XiaomiCloudConnector: 19 | 20 | def __init__(self, username, password): 21 | self._username = username 22 | self._password = password 23 | self._agent = self.generate_agent() 24 | self._device_id = self.generate_device_id() 25 | self._session = requests.session() 26 | self._sign = None 27 | self._ssecurity = None 28 | self.userId = None 29 | self._cUserId = None 30 | self._passToken = None 31 | self._location = None 32 | self._code = None 33 | self._serviceToken = None 34 | 35 | def login_step_1(self): 36 | url = "https://account.xiaomi.com/pass/serviceLogin?sid=xiaomiio&_json=true" 37 | headers = { 38 | "User-Agent": self._agent, 39 | "Content-Type": "application/x-www-form-urlencoded" 40 | } 41 | cookies = { 42 | "userId": self._username 43 | } 44 | response = self._session.get(url, headers=headers, cookies=cookies) 45 | valid = response.status_code == 200 and "_sign" in self.to_json(response.text) 46 | if valid: 47 | self._sign = self.to_json(response.text)["_sign"] 48 | return valid 49 | 50 | def login_step_2(self): 51 | url = "https://account.xiaomi.com/pass/serviceLoginAuth2" 52 | headers = { 53 | "User-Agent": self._agent, 54 | "Content-Type": "application/x-www-form-urlencoded" 55 | } 56 | fields = { 57 | "sid": "xiaomiio", 58 | "hash": hashlib.md5(str.encode(self._password)).hexdigest().upper(), 59 | "callback": "https://sts.api.io.mi.com/sts", 60 | "qs": "%3Fsid%3Dxiaomiio%26_json%3Dtrue", 61 | "user": self._username, 62 | "_sign": self._sign, 63 | "_json": "true" 64 | } 65 | response = self._session.post(url, headers=headers, params=fields) 66 | valid = response is not None and response.status_code == 200 67 | if valid: 68 | json_resp = self.to_json(response.text) 69 | valid = "ssecurity" in json_resp and len(str(json_resp["ssecurity"])) > 4 70 | if valid: 71 | self._ssecurity = json_resp["ssecurity"] 72 | self.userId = json_resp["userId"] 73 | self._cUserId = json_resp["cUserId"] 74 | self._passToken = json_resp["passToken"] 75 | self._location = json_resp["location"] 76 | self._code = json_resp["code"] 77 | else: 78 | if "notificationUrl" in json_resp: 79 | print("Two factor authentication required, please use following url and restart extractor:") 80 | print(json_resp["notificationUrl"]) 81 | print() 82 | return valid 83 | 84 | def login_step_3(self): 85 | headers = { 86 | "User-Agent": self._agent, 87 | "Content-Type": "application/x-www-form-urlencoded" 88 | } 89 | response = self._session.get(self._location, headers=headers) 90 | if response.status_code == 200: 91 | self._serviceToken = response.cookies.get("serviceToken") 92 | return response.status_code == 200 93 | 94 | def login(self): 95 | self._session.cookies.set("sdkVersion", "accountsdk-18.8.15", domain="mi.com") 96 | self._session.cookies.set("sdkVersion", "accountsdk-18.8.15", domain="xiaomi.com") 97 | self._session.cookies.set("deviceId", self._device_id, domain="mi.com") 98 | self._session.cookies.set("deviceId", self._device_id, domain="xiaomi.com") 99 | if self.login_step_1(): 100 | if self.login_step_2(): 101 | if self.login_step_3(): 102 | return True 103 | else: 104 | print("Unable to get service token.") 105 | else: 106 | print("Invalid login or password.") 107 | else: 108 | print("Invalid username.") 109 | return False 110 | 111 | def get_homes(self, country): 112 | url = self.get_api_url(country) + "/v2/homeroom/gethome" 113 | params = { 114 | "data": '{"fg": true, "fetch_share": true, "fetch_share_dev": true, "limit": 300, "app_ver": 7}'} 115 | return self.execute_api_call_encrypted(url, params) 116 | 117 | def get_devices(self, country, home_id, owner_id): 118 | url = self.get_api_url(country) + "/v2/home/home_device_list" 119 | params = { 120 | "data": '{"home_owner": ' + str(owner_id) + 121 | ',"home_id": ' + str(home_id) + 122 | ', "limit": 200, "get_split_device": true, "support_smart_home": true}' 123 | } 124 | return self.execute_api_call_encrypted(url, params) 125 | 126 | def get_dev_cnt(self, country): 127 | url = self.get_api_url(country) + "/v2/user/get_device_cnt" 128 | params = { 129 | "data": '{ "fetch_own": true, "fetch_share": true}' 130 | } 131 | return self.execute_api_call_encrypted(url, params) 132 | 133 | def get_beaconkey(self, country, did): 134 | url = self.get_api_url(country) + "/v2/device/blt_get_beaconkey" 135 | params = { 136 | "data": '{"did":"' + did + '","pdid":1}' 137 | } 138 | return self.execute_api_call_encrypted(url, params) 139 | 140 | def execute_api_call_encrypted(self, url, params): 141 | headers = { 142 | "Accept-Encoding": "identity", 143 | "User-Agent": self._agent, 144 | "Content-Type": "application/x-www-form-urlencoded", 145 | "x-xiaomi-protocal-flag-cli": "PROTOCAL-HTTP2", 146 | "MIOT-ENCRYPT-ALGORITHM": "ENCRYPT-RC4", 147 | } 148 | cookies = { 149 | "userId": str(self.userId), 150 | "yetAnotherServiceToken": str(self._serviceToken), 151 | "serviceToken": str(self._serviceToken), 152 | "locale": "en_GB", 153 | "timezone": "GMT+02:00", 154 | "is_daylight": "1", 155 | "dst_offset": "3600000", 156 | "channel": "MI_APP_STORE" 157 | } 158 | millis = round(time.time() * 1000) 159 | nonce = self.generate_nonce(millis) 160 | signed_nonce = self.signed_nonce(nonce) 161 | fields = self.generate_enc_params(url, "POST", signed_nonce, nonce, params, self._ssecurity) 162 | response = self._session.post(url, headers=headers, cookies=cookies, params=fields) 163 | if response.status_code == 200: 164 | decoded = self.decrypt_rc4(self.signed_nonce(fields["_nonce"]), response.text) 165 | return json.loads(decoded) 166 | return None 167 | 168 | @staticmethod 169 | def get_api_url(country): 170 | return "https://" + ("" if country == "cn" else (country + ".")) + "api.io.mi.com/app" 171 | 172 | def signed_nonce(self, nonce): 173 | hash_object = hashlib.sha256(base64.b64decode(self._ssecurity) + base64.b64decode(nonce)) 174 | return base64.b64encode(hash_object.digest()).decode('utf-8') 175 | 176 | @staticmethod 177 | def signed_nonce_sec(nonce, ssecurity): 178 | hash_object = hashlib.sha256(base64.b64decode(ssecurity) + base64.b64decode(nonce)) 179 | return base64.b64encode(hash_object.digest()).decode('utf-8') 180 | 181 | @staticmethod 182 | def generate_nonce(millis): 183 | nonce_bytes = os.urandom(8) + (int(millis / 60000)).to_bytes(4, byteorder='big') 184 | return base64.b64encode(nonce_bytes).decode() 185 | 186 | @staticmethod 187 | def generate_agent(): 188 | agent_id = "".join(map(lambda i: chr(i), [random.randint(65, 69) for _ in range(13)])) 189 | return f"Android-7.1.1-1.0.0-ONEPLUS A3010-136-{agent_id} APP/xiaomi.smarthome APPV/62830" 190 | 191 | @staticmethod 192 | def generate_device_id(): 193 | return "".join(map(lambda i: chr(i), [random.randint(97, 122) for _ in range(6)])) 194 | 195 | @staticmethod 196 | def generate_signature(url, signed_nonce, nonce, params): 197 | signature_params = [url.split("com")[1], signed_nonce, nonce] 198 | for k, v in params.items(): 199 | signature_params.append(f"{k}={v}") 200 | signature_string = "&".join(signature_params) 201 | signature = hmac.new(base64.b64decode(signed_nonce), msg=signature_string.encode(), digestmod=hashlib.sha256) 202 | return base64.b64encode(signature.digest()).decode() 203 | 204 | @staticmethod 205 | def generate_enc_signature(url, method, signed_nonce, params): 206 | signature_params = [str(method).upper(), url.split("com")[1].replace("/app/", "/")] 207 | for k, v in params.items(): 208 | signature_params.append(f"{k}={v}") 209 | signature_params.append(signed_nonce) 210 | signature_string = "&".join(signature_params) 211 | return base64.b64encode(hashlib.sha1(signature_string.encode('utf-8')).digest()).decode() 212 | 213 | @staticmethod 214 | def generate_enc_params(url, method, signed_nonce, nonce, params, ssecurity): 215 | params['rc4_hash__'] = XiaomiCloudConnector.generate_enc_signature(url, method, signed_nonce, params) 216 | for k, v in params.items(): 217 | params[k] = XiaomiCloudConnector.encrypt_rc4(signed_nonce, v) 218 | params.update({ 219 | 'signature': XiaomiCloudConnector.generate_enc_signature(url, method, signed_nonce, params), 220 | 'ssecurity': ssecurity, 221 | '_nonce': nonce, 222 | }) 223 | return params 224 | 225 | @staticmethod 226 | def to_json(response_text): 227 | return json.loads(response_text.replace("&&&START&&&", "")) 228 | 229 | @staticmethod 230 | def encrypt_rc4(password, payload): 231 | r = ARC4.new(base64.b64decode(password)) 232 | r.encrypt(bytes(1024)) 233 | return base64.b64encode(r.encrypt(payload.encode())).decode() 234 | 235 | @staticmethod 236 | def decrypt_rc4(password, payload): 237 | r = ARC4.new(base64.b64decode(password)) 238 | r.encrypt(bytes(1024)) 239 | return r.encrypt(base64.b64decode(payload)) 240 | 241 | 242 | def print_tabbed(value, tab): 243 | print(" " * tab + value) 244 | 245 | 246 | def print_entry(key, value, tab): 247 | if value: 248 | print_tabbed(f'{key + ":": <10}{value}', tab) 249 | 250 | 251 | def main(): 252 | servers = ["cn", "de", "us", "ru", "tw", "sg", "in", "i2"] 253 | servers_str = ", ".join(servers) 254 | print("Username (email or user ID):") 255 | username = input() 256 | print("Password:") 257 | password = getpass("") 258 | print(f"Server (one of: {servers_str}) Leave empty to check all available:") 259 | server = input() 260 | while server not in ["", *servers]: 261 | print(f"Invalid server provided. Valid values: {servers_str}") 262 | print("Server:") 263 | server = input() 264 | 265 | print() 266 | if not server == "": 267 | servers = [server] 268 | 269 | connector = XiaomiCloudConnector(username, password) 270 | print("Logging in...") 271 | logged = connector.login() 272 | if logged: 273 | print("Logged in.") 274 | print() 275 | for current_server in servers: 276 | hh = [] 277 | homes = connector.get_homes(current_server) 278 | if homes is not None: 279 | for h in homes['result']['homelist']: 280 | hh.append({'home_id': h['id'], 'home_owner': connector.userId}) 281 | dev_cnt = connector.get_dev_cnt(current_server) 282 | if dev_cnt is not None: 283 | for h in dev_cnt["result"]["share"]["share_family"]: 284 | hh.append({'home_id': h['home_id'], 'home_owner': h['home_owner']}) 285 | 286 | if len(hh) == 0: 287 | print(f'No homes found for server "{current_server}".') 288 | continue 289 | 290 | for home in hh: 291 | devices = connector.get_devices(current_server, home['home_id'], home['home_owner']) 292 | if devices is not None: 293 | if devices["result"]["device_info"] is None or len(devices["result"]["device_info"]) == 0: 294 | print(f'No devices found for server "{current_server}" @ home "{home["home_id"]}".') 295 | continue 296 | print(f'Devices found for server "{current_server}" @ home "{home["home_id"]}":') 297 | for device in devices["result"]["device_info"]: 298 | print_tabbed("---------", 3) 299 | if "name" in device: 300 | print_entry("NAME", device["name"], 3) 301 | if "did" in device: 302 | print_entry("ID", device["did"], 3) 303 | if "blt" in device["did"]: 304 | beaconkey = connector.get_beaconkey(current_server, device["did"]) 305 | if beaconkey and "result" in beaconkey and "beaconkey" in beaconkey["result"]: 306 | print_entry("BLE KEY", beaconkey["result"]["beaconkey"], 3) 307 | if "mac" in device: 308 | print_entry("MAC", device["mac"], 3) 309 | if "localip" in device: 310 | print_entry("IP", device["localip"], 3) 311 | if "token" in device: 312 | print_entry("TOKEN", device["token"], 3) 313 | if "model" in device: 314 | print_entry("MODEL", device["model"], 3) 315 | print_tabbed("---------", 3) 316 | print() 317 | else: 318 | print(f"Unable to get devices from server {current_server}.") 319 | else: 320 | print("Unable to log in.") 321 | 322 | print() 323 | print("Press ENTER to finish") 324 | input() 325 | 326 | 327 | if __name__ == "__main__": 328 | main() 329 | -------------------------------------------------------------------------------- /token_extractor.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | block_cipher = None 4 | 5 | 6 | a = Analysis(['token_extractor.py'], 7 | binaries=[], 8 | datas=[], 9 | hiddenimports=[], 10 | hookspath=[], 11 | runtime_hooks=[], 12 | excludes=[], 13 | win_no_prefer_redirects=False, 14 | win_private_assemblies=False, 15 | cipher=block_cipher, 16 | noarchive=False) 17 | pyz = PYZ(a.pure, a.zipped_data, 18 | cipher=block_cipher) 19 | exe = EXE(pyz, 20 | a.scripts, 21 | a.binaries, 22 | a.zipfiles, 23 | a.datas, 24 | [], 25 | name='token_extractor', 26 | debug=False, 27 | bootloader_ignore_signals=False, 28 | strip=False, 29 | upx=True, 30 | upx_exclude=[], 31 | runtime_tmpdir=None, 32 | console=True ) 33 | --------------------------------------------------------------------------------