Last active
May 1, 2023 11:21
-
-
Save Holzhaus/4e469b57735e0faa2c66f71f110fadf6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
import argparse | |
import base64 | |
import binascii | |
import getpass | |
import logging | |
import pprint | |
import re | |
import textwrap | |
import urllib.parse | |
import requests | |
def register(email, password): | |
log = logging.getLogger(__name__) | |
url = 'https://api-user.huami.com/registrations/{email}'.format( | |
email=urllib.parse.quote(email), | |
) | |
r = requests.post(url, allow_redirects=False, data=[ | |
('client_id', 'HuaMi'), | |
('password', password), | |
('redirect_uri', 'https://s3-us-west-2.amazonaws.com/hm-registration/successsignin.html'), | |
('region', 'us-west-2'), | |
('marketing', 'AmazFit'), | |
('country_code', 'US'), | |
('subscriptions', 'marketing'), | |
('name', 'test'), | |
('state', 'REDIRECTION'), | |
('token', 'access'), | |
('token', 'refresh'), | |
]) | |
log.debug('Received response: %r', r.content) | |
if r.status_code >= 400: | |
data = r.json() | |
raise ValueError('Bad request: %s' % data['message']) | |
def login(email, password): | |
log = logging.getLogger(__name__) | |
# Get an access code for logging in | |
url = 'https://api-user.huami.com/registrations/{email}/tokens'.format( | |
email=urllib.parse.quote(email), | |
) | |
r = requests.post(url, allow_redirects=False, data=[ | |
('state', 'REDIRECTION'), | |
('client_id', 'HuaMi'), | |
('password', password), | |
('redirect_uri', 'https://s3-us-west-2.amazonaws.com/hm-registration/successsignin.html'), | |
('region', 'us-west-2'), | |
('token', 'access'), | |
('token', 'refresh'), | |
('country_code', 'US'), | |
]) | |
log.debug('Received response: %r', r.content) | |
if r.status_code >= 400: | |
data = r.json() | |
raise ValueError('Bad request: %s' % data['message']) | |
location = urllib.parse.urlsplit(r.headers['Location']) | |
location_params = urllib.parse.parse_qs(location.query) | |
if 'access' not in location_params: | |
log.error('Access code not found, credentials may be incorrect') | |
raise ValueError('Access code not found') | |
access_code = location_params['access'] | |
# Try to login with access code | |
r = requests.post('https://account.huami.com/v2/client/login', data={ | |
'dn': ','.join(( | |
'account.huami.com', | |
'api-user.huami.com', | |
'app-analytics.huami.com', | |
'api-watch.huami.com', | |
'api-analytics.huami.com', | |
'api-mifit.huami.com', | |
)), | |
'app_version': '4.0.6', | |
'source': 'com.xiaomi.hm.health_4.0.6:7778', | |
'country_code': 'US', | |
'device_id': '123456789012345', | |
'third_name': 'huami', | |
'lang': 'en', | |
'device_model': 'android_phone', | |
'allow_registration': 'false', | |
'app_name': 'com.xiaomi.hm.health', | |
'code': access_code, | |
'grant_type': 'access_token', | |
}) | |
log.debug('Received response: %r', r.content) | |
data = r.json() | |
if 'error_code' in data: | |
if data['error_code'] != '0117': | |
raise ValueError("Error occured!") | |
# This is a new account, register first | |
r = requests.post('https://account.huami.com/v1/client/register', data={ | |
'dn': ','.join(( | |
'account.huami.com', | |
'api-user.huami.com', | |
'app-analytics.huami.com', | |
'api-watch.huami.com', | |
'api-analytics.huami.com', | |
'api-mifit.huami.com', | |
)), | |
'app_version': '4.0.6', | |
'source': 'com.xiaomi.hm.health:4.0.6:7778', | |
'country_code': 'US', | |
'device_id': '123456789012345', | |
'third_name': 'huami', | |
'lang': 'en', | |
'device_model': 'android_phone', | |
'allow_registration': 'false', | |
'app_name': 'com.xiaomi.hm.health', | |
'code': access_code, | |
'grant_type': 'access_token', | |
}) | |
log.debug('Received response: %r', r.content) | |
data = r.json() | |
user_id = data['token_info']['user_id'] | |
app_token = data['token_info']['app_token'] | |
return (user_id, app_token) | |
def pubkeyhash(value): | |
if not re.match(r'^SHA1:\w{32}$', value): | |
raise ValueError( | |
'Invalid pubkeyhash format (valid: "SHA1:<32 hexchars>"') | |
return value | |
def email(value): | |
if not re.match(r'^[^@]+@[^@]+\.[^@]+$', value): | |
raise ValueError('Not a valid email address!') | |
return value | |
def main(argv=None): | |
parser = argparse.ArgumentParser( | |
formatter_class=argparse.RawDescriptionHelpFormatter, | |
description='Request a pairing signature from Huami\'s servers.', | |
epilog=textwrap.dedent(""" | |
To make a request, you'll need the following: | |
- A Huami account (NOT a Mi-Home account) | |
- The SHA1 hash of the Mi Band 4 public key, which looks like this: | |
SHA1:abcdef1234567890aabbccddeeff1122 | |
- 32 bytes of randomness in base64 encoding. e.g.: | |
VGhpcyBpcyAzMiBieXRlcyBvZiByYW5kb20gZGF0YS4= | |
You can request like this: | |
$ ./miband4auth.py -u user@example.com "SHA1:abcde..." "VGh...S4=". | |
If you don't have a Huami account account yet, you can register | |
one by using the -r flag: | |
$ ./miband4auth.py -r -u user@example.com "SHA1:abcde..." "VGh...S4=". | |
DISCLAIMER: Use at your own risk. I'm not responsible for your actions | |
(e.g. Huami TOS violations). | |
""")) | |
parser.add_argument('-r', '--register', action='store_true', | |
help='register a new account') | |
parser.add_argument('-u', '--user', action='store', type=email, | |
help='login email address') | |
parser.add_argument('-p', '--password', action='store', | |
help='login password') | |
parser.add_argument('-v', '--verbose', action='store_true', | |
help='be verbose') | |
parser.add_argument('pubkeyhash', type=pubkeyhash, | |
help='public key hash ("SHA1:abcdef1234...")') | |
parser.add_argument('random', help='random bytes in base64 encoding') | |
args = parser.parse_args(argv) | |
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO) | |
log = logging.getLogger(__name__) | |
if not args.user: | |
log.debug('Username not specified, asking...') | |
while not args.user: | |
try: | |
args.user = email(input('User: ')) | |
except ValueError: | |
print('Not a valid email address!') | |
if not args.password: | |
log.debug('Password not specified, asking...') | |
args.password = getpass.getpass() | |
if args.register: | |
print('Registering new account...') | |
register(args.user, args.password) | |
print('Logging in...') | |
user_id, app_token = login(args.user, args.password) | |
print('User ID: {}'.format(user_id)) | |
print('App Token: {}'.format(app_token)) | |
# We can now access protected resources | |
print('Requesting signature...') | |
r = requests.get( | |
'https://api-mifit-de.huami.com/v1/device/binds.json', | |
headers={ | |
'apptoken': app_token, | |
'appname': 'com.xiaomi.hm.health', | |
}, | |
params={ | |
'userid': user_id, | |
'publickeyhash': args.pubkeyhash, | |
'random': args.random, | |
}) | |
log.debug('Received response: %r', r.content) | |
data = r.json() | |
signature_base64 = data['data']['signature'] | |
signature = base64.b64decode(signature_base64) | |
print('Signature (base64): {}'.format(signature_base64)) | |
print('Signature (Hex): 0x{}'.format(binascii.hexlify(signature).decode())) | |
# Fetch list of registered devices | |
print('Listing registered devices...') | |
r = requests.get( | |
'https://api-mifit-de.huami.com/v1/device/lists.json', | |
headers={ | |
'apptoken': app_token, | |
'appname': 'com.xiaomi.hm.health', | |
}, | |
params={ | |
'userid': user_id, | |
}) | |
log.debug('Received response: %r', r.content) | |
data = r.json() | |
if data['message'] == 'success': | |
if not data['data']: | |
print('No devices are bound to your account.') | |
else: | |
pprint.pprint(data['data']) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I have a question regarding the following two arguments:
pubkeyhash - is this the same for all miBand 4 devices and is obtained with this command at the start of pairing (see code below)
random - in the example you have mentioned that we use 32 bytes of randomness in base64 encoding. However, the device return only 16 bytes when the following command is sent to obtain the auth random key (see code below). Where do you get another 16 bytes of random argument?