homebridge-xbox-tv
Version:
Homebridge plugin to control Xbox game consoles.
165 lines (144 loc) • 7.28 kB
JavaScript
import Packets from './packets.js';
import Structure from './structure.js';
import { LocalApi } from '../constants.js';
class Simple {
constructor(type) {
this.type = type;
this.packets = new Packets();
this.packet = this.packets[type];
this.packetProtected = this.packet.payloadProtected ? this.packets[`${type}Protected`] : false;
}
// === Helpers ===
static applyPKCS7Padding(structure) {
const blockSize = 16;
const length = structure.toBuffer().length;
const padTotal = blockSize - (length % blockSize || blockSize);
for (let i = 0; i < padTotal; i++) {
structure.writeUInt8(padTotal);
}
}
set(key, value, isProtected = false) {
const targetPacket = isProtected ? this.packetProtected : this.packet;
if (!targetPacket || !targetPacket[key]) return;
const currentLength = targetPacket[key].length || 0;
targetPacket[key].value = value;
targetPacket[key].length = currentLength > 0 ? value.length : currentLength;
}
pack(crypto = false) {
const structure = new Structure();
let packet;
let payloadProtectedLength = 0;
let payloadProtectedLengthReal = 0;
for (const name in this.packet) {
if (name === 'payloadProtected' && this.packetProtected) {
const structureProtected = new Structure();
for (const fieldName in this.packetProtected) {
if (this.packet.payloadProtected?.value?.[fieldName] !== undefined) {
this.packetProtected[fieldName].value = this.packet.payloadProtected.value[fieldName];
}
this.packetProtected[fieldName].pack(structureProtected);
}
payloadProtectedLength = structureProtected.toBuffer().length;
Simple.applyPKCS7Padding(structureProtected);
payloadProtectedLengthReal = structureProtected.toBuffer().length;
const payloadEncrypted = crypto.encrypt(
structureProtected.toBuffer(),
crypto.getKey(),
this.packet.iv?.value
);
structure.writeBytes(payloadEncrypted);
} else {
this.packet[name].pack(structure);
}
}
const payload = structure.toBuffer();
switch (this.type) {
case 'powerOn':
// FIX: powerOn (dd02) wire format: [type 2B][unprotectedLen 2B][protectedLen 2B][version=2 2B][payload]
// Original pack1('', ...) produced [type][len][00 00][payload] — missing protectedLen field.
{
const hdr = new Structure();
hdr.writeUInt16(payload.length); // unprotected_payload_length
hdr.writeUInt16(0); // protected_payload_length = 0
hdr.writeUInt16(2); // version = 2
packet = Buffer.concat([LocalApi.Messages.Flags.powerOn, hdr.toBuffer(), payload]);
}
break;
case 'discoveryRequest':
packet = this.pack1(LocalApi.Messages.Flags.discoveryRequest, payload, Buffer.from('0000', 'hex'));
break;
case 'discoveryResponse':
packet = this.pack1(LocalApi.Messages.Flags.discoveryResponse, payload, Buffer.from([0, 2]));
break;
case 'connectRequest':
packet = this.pack1(LocalApi.Messages.Flags.connectRequest, payload, Buffer.from('0002', 'hex'), payloadProtectedLength, payloadProtectedLengthReal);
const payloadProtected = crypto.sign(packet);
packet = Buffer.concat([packet, Buffer.from(payloadProtected)]);
break;
case 'connectRequestProtected':
Simple.applyPKCS7Padding(structure);
// FIX: original passed crypto.getIv() as key — encrypt(data, key, iv) requires key first
let payloadEncrypted = crypto.encrypt(structure.toBuffer(), crypto.getKey(), crypto.getIv());
payloadEncrypted = new Structure(payloadEncrypted);
packet = payloadEncrypted.toBuffer();
break;
case 'connectResponse':
packet = this.pack1(LocalApi.Messages.Flags.connectResponse, payload, Buffer.from([0, 2]));
break;
default:
packet = payload;
}
return packet;
}
pack1(type, payload, version, payloadProtectedLength = 0, payloadProtectedLengthReal = 0) {
const structure = new Structure();
const structureProtected = new Structure();
if (payloadProtectedLength > 0) {
structure.writeUInt16(payload.length - payloadProtectedLengthReal);
const payloadLength = structure.toBuffer();
structureProtected.writeUInt16(payloadProtectedLength);
payloadProtectedLength = structureProtected.toBuffer();
return Buffer.concat([type, payloadLength, payloadProtectedLength, version, payload]);
}
structure.writeUInt16(payload.length);
const payloadLength = structure.toBuffer();
return Buffer.concat([type, payloadLength, Buffer.from([0, version[1] || 0]), payload]);
}
unpack(crypto = undefined, data = false) {
const structure = new Structure(data);
const typeHex = structure.readBytes(2).toString('hex');
const type = typeHex === 'dd02' ? 'powerOn' : this.type;
let packet = {
typeHex,
type,
payloadLength: structure.readUInt16(),
version: structure.readUInt16(),
};
if (packet.version !== 0 && packet.version !== 2) {
packet.payloadProtectedLength = packet.version;
packet.version = structure.readUInt16();
}
for (const name in this.packet) {
packet[name] = this.packet[name].unpack(structure);
this.set(name, packet[name]);
}
if (packet.payloadProtected !== undefined) {
// FIX: extract signature BEFORE truncating the buffer
const signature = packet.payloadProtected.subarray(-32);
const encryptedData = packet.payloadProtected.subarray(0, -32);
// Original: decrypt(data, iv) — matches sgcrypto.decrypt(data, iv, key) signature
const decrypted = crypto.decrypt(encryptedData, packet.iv).subarray(0, packet.payloadProtectedLength);
packet.payloadProtected = {};
const structurePayloadDecrypted = new Structure(decrypted);
const packetProtected = this.packets[`${packet.type}Protected`];
for (const name in packetProtected) {
packet.payloadProtected[name] = packetProtected[name].unpack(structurePayloadDecrypted);
this.set('payloadProtected', packet.payloadProtected);
}
// Note: crypto.verify not implemented in original sgcrypto — skipped intentionally
packet.signature = signature;
}
return packet;
}
}
export default Simple;