Created
August 16, 2020 01:32
-
-
Save kabbi/242a48dde4d3ae548f6d54cf1f987776 to your computer and use it in GitHub Desktop.
Yeelight mesh bulb ble connection / auth PoC
This file contains hidden or 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
const noble = require('@abandonware/noble'); | |
const EC = require('elliptic').ec; | |
const hkdf = require('futoin-hkdf'); | |
const { crc32 } = require('crc'); | |
const ccm = require('aes-ccm'); | |
const ec = EC('p256'); | |
noble.on('stateChange', async (state) => { | |
if (state === 'poweredOn') { | |
console.log('- scanning'); | |
await noble.startScanningAsync(['fe95']); | |
} | |
}); | |
const waitFor = (events) => | |
new Promise((resolve) => { | |
let interval = setInterval(() => { | |
if (events.length === 0) { | |
return; | |
} | |
const data = events.shift(); | |
console.log('>', data.toString('hex')); | |
resolve(data); | |
clearInterval(interval); | |
}, 100); | |
}); | |
const write = async (char, data) => { | |
console.log('<', data.toString('hex')); | |
await char.writeAsync(data, true); | |
}; | |
const hex = (parts) => Buffer.from(parts.join(''), 'hex'); | |
const delay = (time) => new Promise((resolve) => setTimeout(resolve, time)); | |
noble.on('discover', async (peripheral) => { | |
if (peripheral.address !== '50-ec-50-de-36-ac') { | |
return; | |
} | |
console.log('- connecting to', peripheral.address); | |
await noble.stopScanningAsync(); | |
await peripheral.connectAsync(); | |
const { | |
characteristics: [prepareChar, mainChar], | |
} = await peripheral.discoverSomeServicesAndCharacteristicsAsync( | |
['fe95'], | |
['0010', '0016'] | |
); | |
console.log('- connected'); | |
const key = ec.genKeyPair(); | |
await prepareChar.subscribeAsync(); | |
await mainChar.subscribeAsync(); | |
const prepareEvents = []; | |
const mainEvents = []; | |
prepareChar.on('data', (data) => prepareEvents.push(data)); | |
mainChar.on('data', (data) => mainEvents.push(data)); | |
await write(prepareChar, Buffer.of(0xa4)); | |
await waitFor(mainEvents); | |
await write(mainChar, hex`000005000650`); | |
await waitFor(mainEvents); | |
await write( | |
mainChar, | |
hex`00000501505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050` | |
); | |
await delay(200); | |
await write(prepareChar, hex`50000000`); | |
await write(mainChar, hex`000000030100`); | |
await waitFor(mainEvents); | |
await write( | |
mainChar, | |
Buffer.concat([hex`0100`, Buffer.of(...key.getPublic().encode().slice(1))]) | |
); | |
await waitFor(mainEvents); | |
const devicePubKeyData = await waitFor(mainEvents); | |
await write(mainChar, hex`00000300`); | |
const deviceKey = ec.keyFromPublic( | |
Buffer.concat([Buffer.of(4), devicePubKeyData.slice(4)]) | |
); | |
const cloudKey = hex`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`; // 32 bytes ltmk key of your device | |
const secretKey = key.derive(deviceKey.getPublic()).toBuffer(); | |
const sessionKey = hkdf(Buffer.concat([secretKey, cloudKey]), 64, { | |
salt: 'miot-mesh-login-salt', | |
info: 'miot-mesh-login-info', | |
hash: 'sha256', | |
}); | |
const devicePubKeyCRC = Buffer.alloc(4); | |
devicePubKeyCRC.writeUInt32LE(crc32(devicePubKeyData.slice(4))); | |
const { ciphertext, auth_tag } = ccm.encrypt( | |
sessionKey.slice(16, 32), | |
hex`101112131415161718191a1b`, | |
devicePubKeyCRC, | |
Buffer.alloc(0), | |
4 | |
); | |
const authBlob = Buffer.concat([ciphertext, auth_tag]); | |
await write(mainChar, hex`000000050100`); | |
await waitFor(mainEvents); | |
await write(mainChar, Buffer.concat([hex`0100`, authBlob])); | |
await waitFor(mainEvents); | |
await waitFor(prepareEvents); | |
await peripheral.disconnectAsync(); | |
process.exit(0); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment