├── .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 | Platform |
86 | Payment methods |
87 | Link |
88 | Comment |
89 |
90 |
91 | Ko-fi |
92 |
93 | PayPal
94 | Credit card
95 | |
96 |
97 |
98 | |
99 |
100 | No fees
101 | Single or monthly payment
102 | |
103 |
104 |
105 | buycoffee.to |
106 |
107 | BLIK
108 | Bank transfer
109 | |
110 |
111 |
112 | |
113 | |
114 |
115 |
116 | PayPal |
117 |
118 | PayPal
119 | |
120 |
121 |
122 | |
123 |
124 | No fees
125 | |
126 |
127 |
128 | Revolut |
129 |
130 | Revolut
131 | Credit Card
132 | |
133 |
134 |
135 | |
136 |
137 | No fees
138 | |
139 |
140 |
141 |
142 | ### Powered by
143 | [](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 |
--------------------------------------------------------------------------------