sungrow/sungrow.go

157 lines
4 KiB
Go

/*
Copyright 2023 Josh Deprez
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/binary"
"fmt"
"io"
"net"
"sync"
)
const (
privateKey = "Grow#0*2Sun68CbE" // 16 bytes
noCrypto1 = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" // 16 bytes
noCrypto2 = "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" // 16 bytes
getKeyMsg = "\x68\x68\x00\x00\x00\x06\xf7\x04\x0a\xe7\x00\x08"
header = "\x68\x68"
padding = noCrypto2 // all FF bytes
)
func getKey(conn io.ReadWriter) ([]byte, error) {
if _, err := conn.Write([]byte(getKeyMsg)); err != nil {
return nil, err
}
kp := make([]byte, 25)
if _, err := io.ReadFull(conn, kp); err != nil {
return nil, err
}
key := kp[9:] // must be 16 bytes long, because maths
if bytes.Equal(key, []byte(noCrypto1)) || bytes.Equal(key, []byte(noCrypto2)) {
return nil, nil
}
for i := range key {
key[i] ^= privateKey[i]
}
return key, nil
}
// sungrowConn wraps a regular TCP connection with funky Sungrow encryption.
type sungrowConn struct {
net.Conn
block cipher.Block
fifo *bytes.Buffer
mu sync.RWMutex
txid uint16
}
func (c *sungrowConn) Write(msg []byte) (int, error) {
if c.block == nil {
return c.Conn.Write(msg)
}
c.mu.Lock()
defer c.mu.Unlock()
c.txid = binary.BigEndian.Uint16(msg[:2])
c.fifo.Truncate(0)
bs := c.block.BlockSize()
padlen := bs - (len(msg) % bs)
if padlen == bs {
padlen = 0
}
req := make([]byte, 4+len(msg)+padlen)
copy(req, []byte{1, 0, byte(len(msg)), byte(padlen)}) // 4 byte encryption header
copy(req[4:], header) // 2 byte sungrow header, replaces txid
copy(req[6:], msg[2:]) // rest of message
copy(req[4+len(msg):], padding[:padlen]) // padding
// ECB mode.........
for cp := req[4:]; len(cp) > 0; cp = cp[bs:] {
c.block.Encrypt(cp, cp)
}
n, err := c.Conn.Write(req)
return n - 4 - padlen, err
}
func (c *sungrowConn) Read(out []byte) (int, error) {
if c.block == nil {
return c.Conn.Read(out)
}
if c.fifo.Len() > 0 {
c.mu.RLock()
defer c.mu.RUnlock()
return c.fifo.Read(out)
}
// 4-byte header describes how much to read
hdr := make([]byte, 4)
if _, err := io.ReadFull(c.Conn, hdr); err != nil {
return 0, err
}
pktlen, padlen := int(hdr[2]), int(hdr[3])
bs := c.block.BlockSize()
if (pktlen+padlen)%bs != 0 {
return 0, fmt.Errorf("pktlen + padlen = %d + %d = %d, want divisible by %d", pktlen, padlen, pktlen+padlen, bs)
}
pkt := make([]byte, pktlen+padlen)
if _, err := io.ReadFull(c.Conn, pkt); err != nil {
return 0, err
}
// ECB mode here too.........
for cp := pkt; len(cp) > 0; cp = cp[bs:] {
c.block.Decrypt(cp, cp)
}
c.mu.RLock()
defer c.mu.RUnlock()
binary.BigEndian.PutUint16(pkt, c.txid)
c.fifo.Write(pkt[:pktlen])
return c.fifo.Read(out)
}
// dialSungrow dials a TCP connection, obtains the encryption key, and returns
// a sungrowConn that encrypts/decrypts using the key.
func dialSungrow(addr string) (*sungrowConn, error) {
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, fmt.Errorf("dialing: %w", err)
}
key, err := getKey(conn)
if err != nil {
return nil, fmt.Errorf("obtaining key: %w", err)
}
if len(key) == 0 { // no encryption
return &sungrowConn{Conn: conn}, nil
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("setting cipher: %w", err)
}
return &sungrowConn{
Conn: conn,
block: block,
fifo: new(bytes.Buffer),
}, nil
}