diff options
Diffstat (limited to 'inform')
-rw-r--r-- | inform/codec.go | 83 | ||||
-rw-r--r-- | inform/crypto.go | 88 | ||||
-rw-r--r-- | inform/inform.go | 133 | ||||
-rw-r--r-- | inform/rx_messages.go | 99 | ||||
-rw-r--r-- | inform/tx_messages.go | 28 |
5 files changed, 431 insertions, 0 deletions
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 @@ | |||
1 | package inform | ||
2 | |||
3 | import ( | ||
4 | "bytes" | ||
5 | "encoding/binary" | ||
6 | "errors" | ||
7 | "io" | ||
8 | ) | ||
9 | |||
10 | type Codec struct { | ||
11 | // KeyBag contains a mapping of comma-separated MAC addresses to their AES | ||
12 | // keys | ||
13 | KeyBag map[string]string | ||
14 | } | ||
15 | |||
16 | func (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 | |||
55 | func (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 @@ | |||
1 | package inform | ||
2 | |||
3 | import ( | ||
4 | "bytes" | ||
5 | "crypto/aes" | ||
6 | "crypto/cipher" | ||
7 | "crypto/rand" | ||
8 | "encoding/hex" | ||
9 | "errors" | ||
10 | ) | ||
11 | |||
12 | func 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 | |||
18 | func 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 | |||
27 | func 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 | |||
41 | func 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 | ||
50 | func 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 | |||
71 | func 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 @@ | |||
1 | package inform | ||
2 | |||
3 | import ( | ||
4 | "bytes" | ||
5 | "encoding/json" | ||
6 | "errors" | ||
7 | "fmt" | ||
8 | "github.com/mitchellh/mapstructure" | ||
9 | ) | ||
10 | |||
11 | const ( | ||
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 | ||
22 | type 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 | ||
31 | func 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 | ||
41 | func (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 | ||
51 | func (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 | |||
57 | func (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 | |||
71 | func (i *InformWrapper) IsEncrypted() bool { | ||
72 | return i.Flags&ENCRYPTED_FLAG != 0 | ||
73 | } | ||
74 | |||
75 | func (i *InformWrapper) SetEncrypted(e bool) { | ||
76 | if e { | ||
77 | i.Flags |= ENCRYPTED_FLAG | ||
78 | } else { | ||
79 | i.Flags &= ENCRYPTED_FLAG | ||
80 | } | ||
81 | } | ||
82 | |||
83 | func (i *InformWrapper) IsCompressed() bool { | ||
84 | return i.Flags&COMPRESSED_FLAG != 0 | ||
85 | } | ||
86 | |||
87 | func (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 | ||
96 | func (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 | ||
117 | func (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 @@ | |||
1 | package inform | ||
2 | |||
3 | // TODO: Convert string time to time.Time | ||
4 | // Response packet | ||
5 | type NoopMessage struct { | ||
6 | Type string `json:"_type"` | ||
7 | Interval int `json:"interval"` | ||
8 | ServerTimeUTC string `json:"server_time_in_utc"` | ||
9 | } | ||
10 | |||
11 | type AlarmEntry struct { | ||
12 | Tag string `json:"tag"` | ||
13 | Type string `json:"string"` | ||
14 | Value string `json:"val"` // float or int observed | ||
15 | } | ||
16 | |||
17 | type AlarmMessage struct { | ||
18 | Entries []*AlarmEntry `json:"entries"` | ||
19 | Index string `json:"index"` | ||
20 | Id string `json:"sId"` | ||
21 | Time int `json:"time"` | ||
22 | } | ||
23 | |||
24 | type 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 | |||
39 | type 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 | |||
48 | type 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 | ||
74 | type 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 @@ | |||
1 | package inform | ||
2 | |||
3 | type 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 | ||
11 | type 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 | } | ||