From 558a53e9651ef4f89b847fbfd2e4862e9d1d4295 Mon Sep 17 00:00:00 2001 From: Josh Deprez Date: Fri, 6 Jan 2023 17:52:03 +1100 Subject: [PATCH] WIPing intensifies --- plug.go | 162 +++++++++++++++++++++++++++++++++++++++++++-------- plug_test.go | 50 +++++++++++----- 2 files changed, 175 insertions(+), 37 deletions(-) diff --git a/plug.go b/plug.go index 293a427..0dca863 100644 --- a/plug.go +++ b/plug.go @@ -1,38 +1,71 @@ -// Package plugctl provides an API for Kasa smart home switches. +// Package plugctl provides an API for controlling Kasa smart home switches. package plugctl import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" "io" "net" ) -// Plug provides an API for a single Kasa smart home switch. +const ( + reqTurnOn = `{"system":{"set_relay_state":{"state":1}}}` + reqTurnOff = `{"system":{"set_relay_state":{"state":0}}}` + reqGetSysinfo = `{"system":{"get_sysinfo":null}}` + reqGetEMeter = `{"emeter":{"get_realtime":null}}` +) + +// Plug provides an API for a single Kasa smart home plug or switch. type Plug struct { - Addr net.TCPAddr + Addr string } -type msg struct { - System *systemMsg `json:"system,omitempty"` - EMeter *eMeterMsg `json:"emeter,omitempty"` -} - -type eMeterMsg struct { - GetRealtime *getRealtimeMsg `json:"get_realtime"` -} - -type getRealtimeMsg struct{} - -type systemMsg struct { - GetSysInfo *getSysInfoMsg `json:"get_sysinfo"` - SetRelayState *setRelayStateMsg `json:"set_relay_state,omitempty"` -} - -type getSysInfoMsg struct{} - -type setRelayStateMsg struct { - State int `json:"state"` +func (p *Plug) roundTrip(req string) (*response, error) { + conn, err := net.Dial("tcp", p.Addr) + if err != nil { + return nil, fmt.Errorf("dialing plug: %w", err) + } + defer conn.Close() + + lp := []byte{0, 0, 0, 0} + binary.BigEndian.PutUint32(lp, uint32(len(req))) + + // Start by writing length + if _, err := conn.Write(lp); err != nil { + return nil, fmt.Errorf("writing length: %w", err) + } + // Rest of the message is encrypted + enc := newEncrypter(conn) + if _, err := enc.Write([]byte(req)); err != nil { + return nil, fmt.Errorf("writing message: %w", err) + } + + // Read length of response + if _, err := io.ReadFull(conn, lp); err != nil { + return nil, fmt.Errorf("reading length: %w", err) + } + rlen := binary.BigEndian.Uint32(lp) + + // Read the encrypted response + buf := make([]byte, rlen) + if _, err := io.ReadFull(conn, buf); err != nil { + return nil, fmt.Errorf("reading response: %w", err) + } + + // Decrypt and parse the response + var resp *response + dec := json.NewDecoder(newDecrypter(bytes.NewReader(buf))) + if err := dec.Decode(&resp); err != nil { + return nil, fmt.Errorf("reading message: %w", err) + } + + return resp, nil } +// Implements TP Link's weaksauce encryption. +// Do NOT use for actually important secrets. type decryptReader struct { r io.Reader c byte @@ -54,6 +87,8 @@ func (d *decryptReader) Read(b []byte) (int, error) { return n, err } +// Implements TP Link's weaksauce encryption. +// Do NOT use for actually important secrets. type encryptWriter struct { w io.Writer c byte @@ -74,3 +109,84 @@ func (e *encryptWriter) Write(b []byte) (int, error) { } return e.w.Write(eb) } + +/* + "sw_ver": "1.0.3 Build 210506 Rel.105435", + "hw_ver": "1.0", + "model": "KP105(AU)", + "deviceId": "8006CF0E89296D2A5BFBDE11C844BB2B1E2D2B7C", + "oemId": "EAB15315847573797920EFE7E9000562", + "hwId": "120EF751A1A4F170831E6C1F57D97B38", + "rssi": -58, + "longitude_i": 1473005, + "latitude_i": -428429, + "alias": "Car charger", + "status": "new", + "mic_type": "IOT.SMARTPLUGSWITCH", + "feature": "TIM", + "mac": "00:5F:67:01:A5:50", + "updating": 0, + "led_off": 0, + "obd_src": "tplink", + "relay_state": 0, + "on_time": 0, + "active_mode": "schedule", + "icon_hash": "", + "dev_name": "Smart Wi-Fi Plug", + "next_action": { + "type": -1 + }, + "ntc_state": 0, + "err_code": 0 +*/ + +type response struct { + System *systemMsg `json:"system,omitempty"` + EMeter *eMeterMsg `json:"emeter,omitempty"` +} + +type eMeterMsg struct { + GetRealtime *getRealtimeMsg `json:"get_realtime"` +} + +type getRealtimeMsg struct{} + +type systemMsg struct { + GetSysInfo *getSysInfoMsg `json:"get_sysinfo"` + SetRelayState *setRelayStateMsg `json:"set_relay_state,omitempty"` +} + +type getSysInfoMsg struct { + SWVer string `json:"sw_ver,omitempty"` // e.g. "1.0.3 Build 210506 Rel.105435", + HWVer string `json:"hw_ver,omitempty"` // e.g. "1.0", + Model string `json:"model,omitempty"` // e.g. "KP105(AU)", + DeviceID string `json:"deviceId,omitempty"` // e.g. "8006CF0E89296D2A5BFBDE11C844BB2B1E2D2B7C", + OEMID string `json:"oemId,omitempty"` // e.g. "EAB15315847573797920EFE7E9000562", + HWID string `json:"hwId,omitempty"` // e.g. "120EF751A1A4F170831E6C1F57D97B38", + RSSI int `json:"rssi,omitempty"` // e.g. -58, + LongitudeI int `json:"longitude_i,omitempty"` // e.g. 1473005, + LatitudeI int `json:"latitude_i,omitempty"` // e.g. -428429, + Alias string `json:"alias,omitempty"` // e.g. "Car charger", + Status string `json:"status,omitempty"` // e.g. "new", + MICType string `json:"mic_type,omitempty"` // e.g. "IOT.SMARTPLUGSWITCH", + Feature string `json:"feature,omitempty"` // e.g. "TIM", + MAC string `json:"mac,omitempty"` // e.g. "00:5F:67:01:A5:50", + Updating int `json:"updating,omitempty"` // e.g. 0, + LEDOff int `json:"led_off,omitempty"` // e.g. 0, + OBDSrc string `json:"obd_src,omitempty"` // e.g. "tplink", + RelayState int `json:"relay_state,omitempty"` // e.g. 0, + OnTime int `json:"on_time,omitempty"` // e.g. 0, + ActiveMode string `json:"active_mode,omitempty"` // e.g. "schedule", + IconHash string `json:"icon_hash,omitempty"` // e.g. "", + DevName string `json:"dev_name,omitempty"` // e.g. "Smart Wi-Fi Plug", + NextAction struct { + Type int `json:"type,omitempty"` + } `json:"next_action,omitempty"` // e.g. {"type": -1}, + NTCState int `json:"ntc_state,omitempty"` // e.g. 0, + ErrCode int `json:"err_code,omitempty"` // e.g. 0 +} + +type setRelayStateMsg struct { + ErrCode int `json:"err_code,omitempty"` + State int `json:"state,omitempty"` +} diff --git a/plug_test.go b/plug_test.go index 110a16a..7556c8d 100644 --- a/plug_test.go +++ b/plug_test.go @@ -2,6 +2,7 @@ package plugctl import ( "bytes" + "crypto/rand" "encoding/base64" "io" "testing" @@ -14,15 +15,15 @@ func TestDecrypt(t *testing.T) { if err != nil { t.Fatalf("base64.DecodeString(%q) error: %v", in64, err) } + t.Logf("in[:4] = %v", in[:4]) in = in[4:] want := []byte(`{"system":{"set_relay_state":{"state":1}}}`) - dec := newDecrypter(bytes.NewReader(in)) - got, err := io.ReadAll(dec) + got, err := io.ReadAll(newDecrypter(bytes.NewReader(in))) if err != nil { t.Fatalf("io.ReadAll(decryptReader) error: %v", err) } if !bytes.Equal(got, want) { - t.Errorf("decrypt(%02x) = %q, want %q", in, got, want) + t.Errorf("io.ReadAll(decryptReader) = %q, want %q", got, want) } } @@ -36,19 +37,40 @@ func TestEncrypt(t *testing.T) { want = want[4:] in := []byte(`{"system":{"set_relay_state":{"state":1}}}`) var buf bytes.Buffer - newEncrypter(&buf).Write(in) + if _, err := newEncrypter(&buf).Write(in); err != nil { + t.Fatalf("newEncrypter(buf).Write(in) error = %v", err) + } if got := buf.Bytes(); !bytes.Equal(got, want) { - t.Errorf("encrypt(%q) = %02x, want %02x", in, got, want) + t.Errorf("buf.Bytes() = %02x, want %02x", got, want) } } -// func TestEncryptDecrypt(t *testing.T) { -// want := make([]byte, 64) -// if _, err := rand.Read(want); err != nil { -// t.Fatalf("rand.Read(want) error: %v", err) -// } -// if got := decrypt(encrypt(want)); !bytes.Equal(got, want) { -// t.Errorf("decrypt(encrypt(%02x)) = %02x, want %02x", want, got, want) -// } -// } +func TestEncryptDecrypt(t *testing.T) { + want := make([]byte, 64) + if _, err := rand.Read(want); err != nil { + t.Fatalf("rand.Read(want) error: %v", err) + } + + var buf bytes.Buffer + newEncrypter(&buf).Write(want) + got, err := io.ReadAll(newDecrypter(&buf)) + if err != nil { + t.Fatalf("io.ReadAll(newDecrypter(&buf))") + } + if !bytes.Equal(got, want) { + t.Errorf("decrypt(encrypt(%02x)) = %02x, want %02x", want, got, want) + } +} + +func TestReadStatusFromRealPlug(t *testing.T) { + //t.Skip("Hits the real device") + + p := Plug{Addr: "192.168.86.10:9999"} + + resp, err := p.roundTrip(reqGetSysinfo) + if err != nil { + t.Errorf("Plug.roundTrip(%v) error = %v", reqGetSysinfo, err) + } + t.Logf("Plug.roundTrip(%v) = %#v", reqGetSysinfo, *resp.System.GetSysInfo) +}