Skip to content

Instantly share code, notes, and snippets.

@Holzhaus
Last active May 1, 2023 11:21
Show Gist options
  • Save Holzhaus/4e469b57735e0faa2c66f71f110fadf6 to your computer and use it in GitHub Desktop.
Save Holzhaus/4e469b57735e0faa2c66f71f110fadf6 to your computer and use it in GitHub Desktop.
#!/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()
Copy link

ghost commented Sep 19, 2019

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)

Write 01:00 to 00000009-0000-3512-2118-0009af100700 -- start pairing
Receive 10:01:81:01:18:63:c2:cc:e5:d1:59:41:3b:ed:92:c4:b1:63:c2:79 from 00000009-0000-3512-2118-0009af100700
pubkeyhash extracted: 18:63:c2:cc:e5:d1:59:41:3b:ed:92:c4:b1:63:c2:79

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?

Write 82:00:02 to 00000009-0000-3512-2118-0009af100700
Receive 10:82:01:57:3a:0e:2f:ab:36:46:4e:e2:72:f3:51:7c:4c:3c:a7
random auth key extracted: 57:3a:0e:2f:ab:36:46:4e:e2:72:f3:51:7c:4c:3c:a7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment