aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mcrute@gmail.com>2016-09-17 20:44:37 -0700
committerMike Crute <mcrute@gmail.com>2016-09-17 20:48:38 -0700
commit80aac9a2da274b4f539e5b6cafbfd06b1a75c9d9 (patch)
tree41f877f4ebb6ec9ebe4fa6251ee962f07ba5a477
downloadgo-inform-80aac9a2da274b4f539e5b6cafbfd06b1a75c9d9.tar.bz2
go-inform-80aac9a2da274b4f539e5b6cafbfd06b1a75c9d9.tar.xz
go-inform-80aac9a2da274b4f539e5b6cafbfd06b1a75c9d9.zip
Initial import
-rw-r--r--LICENSE.txt27
-rw-r--r--README.md13
-rw-r--r--example.go49
-rw-r--r--inform/codec.go83
-rw-r--r--inform/crypto.go88
-rw-r--r--inform/inform.go133
-rw-r--r--inform/rx_messages.go99
-rw-r--r--inform/tx_messages.go28
8 files changed, 520 insertions, 0 deletions
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..db8d0b9
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,27 @@
1Copyright (c) 2016, Michael Crute
2All rights reserved.
3
4Redistribution and use in source and binary forms, with or without
5modification, are permitted provided that the following conditions are met:
6
71. Redistributions of source code must retain the above copyright notice, this
8 list of conditions and the following disclaimer.
9
102. Redistributions in binary form must reproduce the above copyright notice,
11 this list of conditions and the following disclaimer in the documentation
12 and/or other materials provided with the distribution.
13
143. Neither the name of the copyright holder nor the names of its contributors
15 may be used to endorse or promote products derived from this software without
16 specific prior written permission.
17
18THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e5edeae
--- /dev/null
+++ b/README.md
@@ -0,0 +1,13 @@
1Ubiquiti Inform Protocol in Golang
2==================================
3This repo contains a Golang implemntation of the Ubiquiti Networks Inform
4protocol used by the Unifi access points and the mFi machine networking
5components. The primary focus is an implemenation compatible with mFi
6components but the library should also be compatible with Unifi devices.
7
8This repo is a work in progress and is not yet considered stable. If you find
9it useful patches are welcome, just open a pull request.
10
11There is a feature-complete Python version of this API, a set of tools useful
12for reverse engineering the protocol and an in-progress protocol spec at
13[ubntmfi](https://github.com/mcrute/ubntmfi/blob/master/inform_protocol.md)
diff --git a/example.go b/example.go
new file mode 100644
index 0000000..8e10090
--- /dev/null
+++ b/example.go
@@ -0,0 +1,49 @@
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "github.com/mcrute/go-inform/inform"
7 "io/ioutil"
8 "os"
9)
10
11func main() {
12 fp, err := os.Open("data/test_files/1.bin")
13 if err != nil {
14 fmt.Println("Error loading file")
15 return
16 }
17 defer fp.Close()
18
19 kp, err := os.Open("data/device_keys.json")
20 if err != nil {
21 fmt.Println("Error loading key file")
22 return
23 }
24 defer kp.Close()
25
26 var keys map[string]string
27 kd, _ := ioutil.ReadAll(kp)
28 json.Unmarshal(kd, &keys)
29
30 codec := &inform.Codec{keys}
31
32 msg, err := codec.Unmarshal(fp)
33 if err != nil {
34 fmt.Println(err.Error())
35 return
36 }
37
38 fmt.Printf("%s", msg)
39
40 out, _ := os.Create("test.out")
41 defer out.Close()
42
43 pkt, err := codec.Marshal(msg)
44 if err != nil {
45 fmt.Println(err.Error())
46 return
47 }
48 out.Write(pkt)
49}
diff --git a/inform/codec.go b/inform/codec.go
new file mode 100644
index 0000000..e74c3d7
--- /dev/null
+++ b/inform/codec.go
@@ -0,0 +1,83 @@
1package inform
2
3import (
4 "bytes"
5 "encoding/binary"
6 "errors"
7 "io"
8)
9
10type Codec struct {
11 // KeyBag contains a mapping of comma-separated MAC addresses to their AES
12 // keys
13 KeyBag map[string]string
14}
15
16func (c *Codec) Unmarshal(fp io.Reader) (*InformWrapper, error) {
17 w := NewInformWrapper()
18
19 var magic int32
20 binary.Read(fp, binary.BigEndian, &magic)
21 if magic != PROTOCOL_MAGIC {
22 return nil, errors.New("Invalid magic number")
23 }
24
25 binary.Read(fp, binary.BigEndian, &w.Version)
26 io.ReadFull(fp, w.MacAddr)
27 binary.Read(fp, binary.BigEndian, &w.Flags)
28
29 iv := make([]byte, 16)
30 io.ReadFull(fp, iv)
31
32 binary.Read(fp, binary.BigEndian, &w.DataVersion)
33
34 var dataLen int32
35 binary.Read(fp, binary.BigEndian, &dataLen)
36
37 p := make([]byte, dataLen)
38 io.ReadFull(fp, p)
39
40 key, ok := c.KeyBag[w.FormattedMac()]
41 if !ok {
42 return nil, errors.New("No key found")
43 }
44
45 u, err := Decrypt(p, iv, key)
46 if err != nil {
47 return nil, err
48 }
49
50 w.Payload = u
51
52 return w, nil
53}
54
55func (c *Codec) Marshal(msg *InformWrapper) ([]byte, error) {
56 b := &bytes.Buffer{}
57 payload := msg.Payload
58 var iv []byte
59
60 if msg.IsEncrypted() {
61 key, ok := c.KeyBag[msg.FormattedMac()]
62 if !ok {
63 return nil, errors.New("No key found")
64 }
65
66 var err error
67 payload, iv, err = Encrypt(payload, key)
68 if err != nil {
69 return nil, err
70 }
71 }
72
73 binary.Write(b, binary.BigEndian, PROTOCOL_MAGIC)
74 binary.Write(b, binary.BigEndian, msg.Version)
75 b.Write(msg.MacAddr)
76 binary.Write(b, binary.BigEndian, msg.Flags)
77 b.Write(iv)
78 binary.Write(b, binary.BigEndian, msg.DataVersion)
79 binary.Write(b, binary.BigEndian, int32(len(payload)))
80 b.Write(payload)
81
82 return b.Bytes(), nil
83}
diff --git a/inform/crypto.go b/inform/crypto.go
new file mode 100644
index 0000000..90118c1
--- /dev/null
+++ b/inform/crypto.go
@@ -0,0 +1,88 @@
1package inform
2
3import (
4 "bytes"
5 "crypto/aes"
6 "crypto/cipher"
7 "crypto/rand"
8 "encoding/hex"
9 "errors"
10)
11
12func Pad(src []byte, blockSize int) []byte {
13 padLen := blockSize - (len(src) % blockSize)
14 padText := bytes.Repeat([]byte{byte(padLen)}, padLen)
15 return append(src, padText...)
16}
17
18func Unpad(src []byte, blockSize int) ([]byte, error) {
19 srcLen := len(src)
20 paddingLen := int(src[srcLen-1])
21 if paddingLen >= srcLen || paddingLen > blockSize {
22 return nil, errors.New("Padding size error")
23 }
24 return src[:srcLen-paddingLen], nil
25}
26
27func decodeHexKey(key string) (cipher.Block, error) {
28 decodedKey, err := hex.DecodeString(key)
29 if err != nil {
30 return nil, err
31 }
32
33 block, err := aes.NewCipher(decodedKey)
34 if err != nil {
35 return nil, err
36 }
37
38 return block, nil
39}
40
41func makeAESIV() ([]byte, error) {
42 iv := make([]byte, 16)
43 if _, err := rand.Read(iv); err != nil {
44 return nil, err
45 }
46 return iv, nil
47}
48
49// Returns ciphertext and IV, does not modify payload
50func Encrypt(payload []byte, key string) ([]byte, []byte, error) {
51 ct := make([]byte, len(payload))
52 copy(ct, payload)
53 ct = Pad(ct, aes.BlockSize)
54
55 iv, err := makeAESIV()
56 if err != nil {
57 return nil, nil, err
58 }
59
60 block, err := decodeHexKey(key)
61 if err != nil {
62 return nil, nil, err
63 }
64
65 mode := cipher.NewCBCEncrypter(block, iv)
66 mode.CryptBlocks(ct, ct)
67
68 return ct, iv, nil
69}
70
71func Decrypt(payload, iv []byte, key string) ([]byte, error) {
72 b := make([]byte, len(payload))
73
74 block, err := decodeHexKey(key)
75 if err != nil {
76 return nil, err
77 }
78
79 mode := cipher.NewCBCDecrypter(block, iv)
80 mode.CryptBlocks(b, payload)
81
82 u, err := Unpad(b, aes.BlockSize)
83 if err != nil {
84 return nil, err
85 }
86
87 return u, nil
88}
diff --git a/inform/inform.go b/inform/inform.go
new file mode 100644
index 0000000..b159084
--- /dev/null
+++ b/inform/inform.go
@@ -0,0 +1,133 @@
1package inform
2
3import (
4 "bytes"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "github.com/mitchellh/mapstructure"
9)
10
11const (
12 PROTOCOL_MAGIC int32 = 1414414933 // TNBU
13 INFORM_VERSION int32 = 0
14 DATA_VERSION int32 = 1
15
16 ENCRYPTED_FLAG = 1
17 COMPRESSED_FLAG = 2
18)
19
20// Wrapper around an inform message, serializes directly into the wire
21// protocol
22type InformWrapper struct {
23 Version int32
24 MacAddr []byte
25 Flags int16
26 DataVersion int32
27 Payload []byte
28}
29
30// Create InformWrapper with sane defaults
31func NewInformWrapper() *InformWrapper {
32 return &InformWrapper{
33 Version: INFORM_VERSION,
34 MacAddr: make([]byte, 6),
35 Flags: 0,
36 DataVersion: DATA_VERSION,
37 }
38}
39
40// Update the payload data with JSON value
41func (i *InformWrapper) UpdatePayload(v interface{}) error {
42 if d, err := json.Marshal(v); err != nil {
43 return err
44 } else {
45 i.Payload = d
46 return nil
47 }
48}
49
50// Format Mac address bytes as lowercase string with colons
51func (i *InformWrapper) FormattedMac() string {
52 return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x",
53 i.MacAddr[0], i.MacAddr[1], i.MacAddr[2],
54 i.MacAddr[3], i.MacAddr[4], i.MacAddr[5])
55}
56
57func (i *InformWrapper) String() string {
58 b := &bytes.Buffer{}
59
60 fmt.Fprintf(b, "Version: \t%d\n", i.Version)
61 fmt.Fprintf(b, "Mac Addr: \t%s\n", i.FormattedMac())
62 fmt.Fprintf(b, "Flags: \t%d\n", i.Flags)
63 fmt.Fprintf(b, " Encrypted: \t%t\n", i.IsEncrypted())
64 fmt.Fprintf(b, " Compressed: \t%t\n", i.IsCompressed())
65 fmt.Fprintf(b, "Data Version: \t%d\n", i.DataVersion)
66 fmt.Fprintf(b, "Payload: \t%q\n", i.Payload)
67
68 return b.String()
69}
70
71func (i *InformWrapper) IsEncrypted() bool {
72 return i.Flags&ENCRYPTED_FLAG != 0
73}
74
75func (i *InformWrapper) SetEncrypted(e bool) {
76 if e {
77 i.Flags |= ENCRYPTED_FLAG
78 } else {
79 i.Flags &= ENCRYPTED_FLAG
80 }
81}
82
83func (i *InformWrapper) IsCompressed() bool {
84 return i.Flags&COMPRESSED_FLAG != 0
85}
86
87func (i *InformWrapper) SetCompressed(c bool) {
88 if c {
89 i.Flags |= COMPRESSED_FLAG
90 } else {
91 i.Flags &= COMPRESSED_FLAG
92 }
93}
94
95// Decode payload to a map and try to determine the inform type
96func (i *InformWrapper) decodePayload() (map[string]interface{}, string, error) {
97 var m map[string]interface{}
98
99 if err := json.Unmarshal(i.Payload, &m); err != nil {
100 return nil, "", err
101 }
102
103 t, ok := m["_type"]
104 if !ok {
105 return nil, "", errors.New("Message contains no type")
106 }
107
108 st, ok := t.(string)
109 if !ok {
110 return nil, "", errors.New("Message type is not a string")
111 }
112
113 return m, st, nil
114}
115
116// Decode payload JSON as a inform message
117func (i *InformWrapper) JsonMessage() (interface{}, error) {
118 msg, t, err := i.decodePayload()
119 if err != nil {
120 return nil, err
121 }
122
123 switch t {
124 case "noop":
125 var o NoopMessage
126 if err := mapstructure.Decode(msg, &o); err != nil {
127 return nil, err
128 }
129 return o, nil
130 }
131
132 return nil, errors.New(fmt.Sprintf("Message type %s is invalid", t))
133}
diff --git a/inform/rx_messages.go b/inform/rx_messages.go
new file mode 100644
index 0000000..3f80fcd
--- /dev/null
+++ b/inform/rx_messages.go
@@ -0,0 +1,99 @@
1package inform
2
3// TODO: Convert string time to time.Time
4// Response packet
5type NoopMessage struct {
6 Type string `json:"_type"`
7 Interval int `json:"interval"`
8 ServerTimeUTC string `json:"server_time_in_utc"`
9}
10
11type AlarmEntry struct {
12 Tag string `json:"tag"`
13 Type string `json:"string"`
14 Value string `json:"val"` // float or int observed
15}
16
17type AlarmMessage struct {
18 Entries []*AlarmEntry `json:"entries"`
19 Index string `json:"index"`
20 Id string `json:"sId"`
21 Time int `json:"time"`
22}
23
24type InterfaceMessage struct {
25 IP string `json:"ip"`
26 MacAddress string `json:"mac"`
27 Name string `json:"name"`
28 Type string `json:"type"`
29 ReceivedBytes int `json:"rx_bytes"`
30 ReceivedDropped int `json:"rx_dropped"`
31 ReceivedErrors int `json:"rx_errors"`
32 ReceivedPackets int `json:"rx_packets"`
33 TransmittedBytes int `json:"tx_bytes"`
34 TransmittedDropped int `json:"tx_dropped"`
35 TransmittedErrors int `json:"tx_errors"`
36 TransmittedPackets int `json:"tx_packets"`
37}
38
39type RadioMessage struct {
40 Gain int `json:"builtin_ant_gain"`
41 BuiltinAntenna bool `json:"builtin_antenna"`
42 MaxTransmitPower int `json:"max_txpower"`
43 Name string `json:"name"`
44 RadioProfile string `json:"radio"`
45 // "scan_table": []
46}
47
48type AccessPointMessage struct {
49 BasicSSID string `json:"bssid"`
50 ExtendedSSID string `json:"essid"`
51 ClientConnectionQuality int `json:"ccq"`
52 Channel int `json:"channel"`
53 Id string `json:"id"`
54 Name string `json:"name"`
55 StationNumber string `json:"num_sta"` // int?
56 RadioProfile string `json:"radio"`
57 Usage string `json:"usage"`
58 ReceivedBytes int `json:"rx_bytes"`
59 ReceivedDropped int `json:"rx_dropped"`
60 ReceivedErrors int `json:"rx_errors"`
61 ReceivedPackets int `json:"rx_packets"`
62 ReceivedCrypts int `json:"rx_crypts"`
63 ReceivedFragments int `json:"rx_frags"`
64 ReceivedNetworkIDs int `json:"rx_nwids"`
65 TransmittedBytes int `json:"tx_bytes"`
66 TransmittedDropped int `json:"tx_dropped"`
67 TransmittedErrors int `json:"tx_errors"`
68 TransmittedPackets int `json:"tx_packets"`
69 TransmitPower int `json:"tx_power"`
70 TransmitRetries int `json:"tx_retries"`
71}
72
73// TODO: Convert time to time.Time
74type IncomingMessage struct {
75 Alarms []*AlarmMessage `json:"alarm"`
76 ConfigVersion string `json:"cfgversion"`
77 Default bool `json:"default"`
78 GuestToken string `json:"guest_token"`
79 Hostname string `json:"hostname"`
80 InformURL string `json:"inform_url"`
81 IP string `json:"ip"`
82 Isolated bool `json:"isolated"`
83 LocalVersion string `json:"localversion"`
84 Locating bool `json:"locating"`
85 MacAddress string `json:"mac"`
86 IsMfi string `json:"mfi"` // boolean as string
87 Model string `json:"model"`
88 ModelDisplay string `json:"model_display"`
89 PortVersion string `json:"portversion"`
90 Version string `json:"version"`
91 Serial string `json:"serial"`
92 Time int `json:"time"`
93 Trackable string `json:"trackable"` // boolean as string
94 Uplink string `json:"uplink"`
95 Uptime int `json:"uptime"`
96 Interfaces []*InterfaceMessage `json:"if_table"`
97 Radios []*RadioMessage `json:"radio_table"`
98 AccessPoints []*AccessPointMessage `json:"vap_table"`
99}
diff --git a/inform/tx_messages.go b/inform/tx_messages.go
new file mode 100644
index 0000000..5efddd6
--- /dev/null
+++ b/inform/tx_messages.go
@@ -0,0 +1,28 @@
1package inform
2
3type AdminMetadata struct {
4 Id string `json:"_id"`
5 Language string `json:"lang"`
6 Username string `json:"name"`
7 Password string `json:"x_password"`
8}
9
10// TODO: Convert string time to time.Time
11type CommandMessage struct {
12 Metadata *AdminMetadata `json:"_admin"`
13 Id string `json:"_id"`
14 Type string `json:"_type"` // cmd
15 Command string `json:"cmd"` // mfi-output
16 DateTime string `json:"datetime"` // 2016-07-28T01:17:55Z
17 DeviceId string `json:"device_id"`
18 MacAddress string `json:"mac"`
19 Model string `json:"model"`
20 OffVoltage int `json:"off_volt"`
21 Port int `json:"port"`
22 MessageId string `json:"sId"` // ??
23 ServerTime string `json:"server_time_in_utc"`
24 Time string `json:"time"`
25 Timer int `json:"timer"`
26 Value int `json:"val"`
27 Voltage int `json:"volt"`
28}