├── .gitignore ├── index.js ├── package.json ├── Readme.md ├── extract-cli.js └── src ├── Auth.js └── Api.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const AuthMiIO = require('./src/Auth'); 2 | const ApiMiIO = require('./src/Api'); 3 | 4 | module.exports = { AuthMiIO, ApiMiIO }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miio-token-extractor", 3 | "version": "1.0.0", 4 | "description": "This tool/script retrieves tokens for all devices connected to Xiaomi cloud", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/alexpts/nodejs-miio-token-extractor" 9 | }, 10 | "author": "alexpts", 11 | "license": "MIT", 12 | "keywords": [ 13 | "miio", 14 | "token", 15 | "mijia", 16 | "extractor" 17 | ], 18 | "dependencies": { 19 | "cookie": "^0.4.2", 20 | "node-fetch": "^2.7.0" 21 | }, 22 | "devDependencies": { 23 | "inquirer": "^8.2.6" 24 | }, 25 | "engines": { 26 | "node": ">=14" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Xiaomi Cloud Tokens Extractor 2 | 3 | This tool/script retrieves tokens for all devices connected to Xiaomi cloud and encryption keys for BLE devices. 4 | 5 | You will need to provide Xiaomi Home credentials: 6 | - username (email or Xiaomi Cloud account ID) 7 | - password 8 | - Xiaomi's server region (`cn` - China, `de` - Germany etc.). Leave empty to check all available 9 | 10 | In return all of your devices connected to account will be listed, together with their name and IP address. 11 | 12 | 13 | 14 | ### API definitions 15 | [list devides](https://miot-spec.org/miot-spec-v2/instances) 16 | 17 | [heater zhimi-za2 examole](https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:heater:0000A01A:zhimi-za2:1) 18 | -------------------------------------------------------------------------------- /extract-cli.js: -------------------------------------------------------------------------------- 1 | const inquirer = require("inquirer"); 2 | let { AuthMiIO, ApiMiIO } = require('./index'); 3 | 4 | let authMiIO = new AuthMiIO; 5 | let apiMiIO = new ApiMiIO; 6 | 7 | let inputPrompt = [ 8 | { 9 | name: 'country', 10 | message: 'Your country: ', 11 | type: 'list', 12 | default: "cn", 13 | choices: [ 14 | { name: "China", value: "cn"}, 15 | { name: "Russia", value: "ru"}, 16 | { name: "USA", value: "us"}, 17 | { name: "Taiwan", value: "tw"}, 18 | { name: "Singapore", value: "sg"}, 19 | { name: "Germany", value: "de"}, 20 | { name: "India", value: "in"}, 21 | { name: "India", value: "i2"}, 22 | ] 23 | }, 24 | { 25 | name: 'login', 26 | message: 'Your login (userId/email/phone):', 27 | type: 'string', 28 | }, 29 | { 30 | name: 'password', 31 | message: 'Your password: ', 32 | type: 'password', 33 | }, 34 | ]; 35 | 36 | (async () => { 37 | let { login, password, country } = await inquirer.prompt(inputPrompt); 38 | console.log('Auth...'); 39 | let { userId, token, ssecurity } = await authMiIO.login(login, password); 40 | 41 | console.log('Get devises list...'); 42 | let devices = await apiMiIO.getDeviceList(userId, ssecurity, token, country); 43 | devices = devices.map(device => { 44 | let { did, token, name, localip, model, mac } = device; 45 | return { did, token, name, localip, model, mac }; 46 | }); 47 | 48 | console.log(devices); 49 | })(); -------------------------------------------------------------------------------- /src/Auth.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | const cookie = require('cookie'); 3 | const crypto = require('crypto'); 4 | 5 | 6 | module.exports = class MiIOAuth { 7 | 8 | #loginUrl = 'https://account.xiaomi.com/pass/serviceLoginAuth2'; 9 | 10 | /** 11 | * @param {string} password 12 | * @return {string} 13 | */ 14 | #getPasswordHash(password) { 15 | return crypto.createHash('md5').update(password).digest("hex").toUpperCase(); 16 | } 17 | 18 | async login(login, password) { 19 | let data = new URLSearchParams; 20 | data.append('sid', 'xiaomiio'); 21 | data.append('hash', this.#getPasswordHash(password)); 22 | data.append('user', login); 23 | data.append('_json', 'true'); 24 | 25 | let response = await fetch(this.#loginUrl, { 26 | method: 'POST', 27 | body: data, 28 | }); 29 | 30 | let json = await this.#responseToJson(response); 31 | let { userId, ssecurity, location } = json; 32 | 33 | if (!ssecurity || !location ) { 34 | throw new Error('Can`t login'); 35 | } 36 | 37 | return { 38 | ssecurity, 39 | token: await this.#getServiceToken(location), 40 | userId, 41 | }; 42 | } 43 | 44 | async #getServiceToken(location) { 45 | let response = await fetch(location); 46 | if (response.status !== 200) { 47 | throw new Error('Can`t get service token'); 48 | } 49 | 50 | let cookies = {}; 51 | response.headers.raw()['set-cookie'].forEach((item) => { 52 | cookies = {...cookies, ...cookie.parse(item)}; 53 | }); 54 | 55 | let token = cookies['serviceToken'] || false; 56 | if (!token) { 57 | throw new Error('Can`t get service token from cookies'); 58 | } 59 | 60 | return token; 61 | } 62 | 63 | async #responseToJson(response) { 64 | let body = await response.text(); 65 | if (body.indexOf('&&&START&&&') === 0) { 66 | body = body.substring(11); 67 | return JSON.parse(body); 68 | } 69 | 70 | throw new Error('Can`t login'); 71 | } 72 | 73 | 74 | async getSign(){ 75 | const url = 'https://account.xiaomi.com/pass/serviceLogin?sid=xiaomiio&_json=true'; 76 | const response = await fetch(url); 77 | 78 | let json = await this.#responseToJson(response); 79 | if ('_sign' in json) { 80 | return json['_sign']; 81 | } 82 | 83 | throw new Error('Can`t get sign'); 84 | }; 85 | } -------------------------------------------------------------------------------- /src/Api.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | const crypto = require('crypto'); 3 | 4 | 5 | module.exports = class MiIOApi { 6 | 7 | #apiUrl = 'https://api.io.mi.com/app'; 8 | #allowCounty = ["", "ru", "us", "tw", "sg", "cn", "de", "in", "i2"]; 9 | 10 | constructor(country = '') { 11 | this.#apiUrl = this.getApiUrl(country); 12 | } 13 | 14 | getApiUrl(country) { 15 | if (this.#allowCounty.includes(country) === false) { 16 | throw new Error('Country is not allow'); 17 | } 18 | 19 | let countryDomain = country ? 20 | country === 'cn' ? '' : country + '.' 21 | : ''; 22 | 23 | return `https://${countryDomain}api.io.mi.com/app`; 24 | } 25 | 26 | generateNonce = (security) => { 27 | let nonce = crypto.randomBytes(12); 28 | let value = Buffer.from(security, "base64"); 29 | value = Buffer.concat([value, nonce]); 30 | 31 | return { 32 | nonce: nonce.toString('base64'), 33 | signedNonce: crypto.createHash('sha256').update(value).digest('base64'), 34 | }; 35 | } 36 | 37 | /** 38 | * @param {string} uri 39 | * @param {string} signedNonce 40 | * @param {string} nonce 41 | * @param {{}} data 42 | * @return {string} 43 | */ 44 | generateSignature(uri, signedNonce, nonce, data = {}) { 45 | let signatureParams = [uri, signedNonce, nonce, `data=${JSON.stringify(data)}`]; 46 | let signedString = signatureParams.join('&'); 47 | 48 | return crypto.createHmac('sha256', Buffer.from(signedNonce, "base64")) 49 | .update(signedString) 50 | .digest('base64'); 51 | } 52 | 53 | async getDeviceList(userId, security, token, country = null) { 54 | let url = country === null ? this.#apiUrl : this.getApiUrl(country); 55 | 56 | let { nonce, signedNonce } = this.generateNonce(security); 57 | let data = {getVirtualModel: false, getHuamiDevices: 0}; 58 | 59 | let action = '/home/device_list'; 60 | let signature = this.generateSignature(action, signedNonce, nonce, data); 61 | 62 | let body = new URLSearchParams; 63 | body.append('_nonce', nonce); 64 | body.append('signature', signature); 65 | body.append('data', JSON.stringify(data)); 66 | 67 | let response = await fetch(url + action, { 68 | method: 'POST', 69 | headers: { 70 | 'x-xiaomi-protocal-flag-cli': 'PROTOCAL-HTTP2', 71 | 'Cookie': `userId=${userId}; serviceToken=${token}`, 72 | }, 73 | body: body 74 | }); 75 | 76 | let json = await response.json(); 77 | return json.result.list; 78 | } 79 | } --------------------------------------------------------------------------------