/** * Tapo Device Info Test * * This script polls each Tapo device type to see what data is available. */ import 'dotenv/config'; import crypto from 'crypto'; import http from 'http'; // KLAP Cipher class KlapCipher { constructor(localSeed, remoteSeed, authHash) { const localHash = Buffer.concat([localSeed, remoteSeed, authHash]); this.key = this._keyDerive(localHash); const { iv, seq } = this._ivDerive(localHash); this.iv = iv; this.seq = seq; this.sig = this._sigDerive(localHash); } static sha1(data) { return crypto.createHash('sha1').update(data).digest(); } static sha256(data) { return crypto.createHash('sha256').update(data).digest(); } _keyDerive(h) { return KlapCipher.sha256(Buffer.concat([Buffer.from('lsk'), h])).subarray(0, 16); } _ivDerive(h) { const hash = KlapCipher.sha256(Buffer.concat([Buffer.from('iv'), h])); return { iv: hash.subarray(0, 12), seq: hash.readInt32BE(hash.length - 4) }; } _sigDerive(h) { return KlapCipher.sha256(Buffer.concat([Buffer.from('ldk'), h])).subarray(0, 28); } _ivSeq(seq) { const b = Buffer.alloc(4); b.writeInt32BE(seq); return Buffer.concat([this.iv, b]); } encrypt(data) { this.seq++; const cipher = crypto.createCipheriv('aes-128-cbc', this.key, this._ivSeq(this.seq)); const encrypted = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]); const sigData = Buffer.concat([this.sig, Buffer.from([(this.seq >> 24) & 0xff, (this.seq >> 16) & 0xff, (this.seq >> 8) & 0xff, this.seq & 0xff]), encrypted]); return { payload: Buffer.concat([KlapCipher.sha256(sigData), encrypted]), seq: this.seq }; } decrypt(seq, data) { const decipher = crypto.createDecipheriv('aes-128-cbc', this.key, this._ivSeq(seq)); return Buffer.concat([decipher.update(data.subarray(32)), decipher.final()]).toString('utf8'); } } function httpRequest(ip, path, body, cookie = null) { return new Promise((resolve, reject) => { const options = { hostname: ip, port: 80, path: `/app${path}`, method: 'POST', headers: { 'Content-Type': 'application/octet-stream', 'Content-Length': body.length }, timeout: 5000 }; if (cookie) options.headers['Cookie'] = cookie; const req = http.request(options, (res) => { const chunks = []; res.on('data', chunk => chunks.push(chunk)); res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) })); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); }); req.write(body); req.end(); }); } async function connectDevice(ip, username, password) { const authHash = KlapCipher.sha256(Buffer.concat([ KlapCipher.sha1(Buffer.from(username.toLowerCase().trim())), KlapCipher.sha1(Buffer.from(password)) ])); const localSeed = crypto.randomBytes(16); const hs1 = await httpRequest(ip, '/handshake1', localSeed); if (hs1.status !== 200) throw new Error(`Handshake1 failed: ${hs1.status}`); const cookies = hs1.headers['set-cookie']; const cookieStr = Array.isArray(cookies) ? cookies[0] : cookies; const cookie = cookieStr.match(/TP_SESSIONID=([^;]+)/)?.[0] || ''; const remoteSeed = hs1.body.subarray(0, 16); const serverHash = hs1.body.subarray(16, 48); const localHash = KlapCipher.sha256(Buffer.concat([localSeed, remoteSeed, authHash])); if (!localHash.equals(serverHash)) throw new Error('Auth failed'); const hs2Payload = KlapCipher.sha256(Buffer.concat([remoteSeed, localSeed, authHash])); const hs2 = await httpRequest(ip, '/handshake2', hs2Payload, cookie); if (hs2.status !== 200) throw new Error(`Handshake2 failed: ${hs2.status}`); return { cipher: new KlapCipher(localSeed, remoteSeed, authHash), cookie }; } async function request(ip, cipher, cookie, method, params = {}) { const requestData = JSON.stringify({ method, params }); const { payload, seq } = cipher.encrypt(requestData); const response = await httpRequest(ip, `/request?seq=${seq}`, payload, cookie); if (response.status !== 200) throw new Error(`Request failed: ${response.status}`); return JSON.parse(cipher.decrypt(seq, response.body)); } async function testDevice(ip, type) { console.log(`\n${'='.repeat(60)}`); console.log(`Testing ${type} at ${ip}`); console.log('='.repeat(60)); try { const { cipher, cookie } = await connectDevice(ip, process.env.TAPO_USERNAME, process.env.TAPO_PASSWORD); // Get device info const info = await request(ip, cipher, cookie, 'get_device_info'); console.log('\nšŸ“± get_device_info result:'); console.log(JSON.stringify(info.result, null, 2)); // For hubs, get child devices if (type.includes('H100')) { const children = await request(ip, cipher, cookie, 'get_child_device_list'); console.log('\nšŸ‘¶ get_child_device_list result:'); console.log(JSON.stringify(children.result, null, 2)); } } catch (e) { console.log(`āŒ Error: ${e.message}`); } } async function main() { console.log('Tapo Device Info Test'); console.log('Username:', process.env.TAPO_USERNAME); // Test each device type await testDevice('192.168.3.18', 'P100'); await testDevice('192.168.3.21', 'P115(EU)'); await testDevice('192.168.3.28', 'H100(EU)'); await testDevice('192.168.3.45', 'C200'); await testDevice('192.168.3.22', 'C200 #2'); } main().catch(console.error);