aboutsummaryrefslogtreecommitdiff
path: root/python/inform.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/inform.py')
-rw-r--r--python/inform.py211
1 files changed, 211 insertions, 0 deletions
diff --git a/python/inform.py b/python/inform.py
new file mode 100644
index 0000000..ee47a69
--- /dev/null
+++ b/python/inform.py
@@ -0,0 +1,211 @@
1import json
2import copy
3import struct
4import binascii
5from Crypto.Cipher import AES
6from cStringIO import StringIO
7
8
9class BinaryDataStream(object):
10 """Directional binary data stream
11
12 Reads and writes binary data from any stream-like object. This object is
13 not bi-directional. Does no interpertation just unpacking and packing.
14 """
15
16 def __init__(self, data):
17 self.data = data
18
19 @classmethod
20 def for_output(cls):
21 return cls(StringIO())
22
23 def read_int(self):
24 return struct.unpack(">i", self.data.read(4))[0]
25
26 def read_short(self):
27 return struct.unpack(">h", self.data.read(2))[0]
28
29 def read_string(self, length):
30 return self.data.read(length)
31
32 def write_int(self, data):
33 self.data.write(struct.pack(">i", data))
34
35 def write_short(self, data):
36 self.data.write(struct.pack(">h", data))
37
38 def write_string(self, data):
39 self.data.write(data)
40
41 def get_output(self):
42 return self.data.getvalue()
43
44
45class Cryptor(object):
46 """AES encryption strategy
47
48 Handles AES crypto by wrapping pycrypto. Does padding and un-padding as
49 well as key conversions when needed.
50 """
51
52 def __init__(self, key, iv):
53 self.iv = iv
54 self.key = key
55 self.cipher = AES.new(key.decode("hex"), AES.MODE_CBC, iv)
56
57 @staticmethod
58 def unpad(s):
59 return s[0:-ord(s[-1])]
60
61 @staticmethod
62 def pad(s, BS=16):
63 return s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
64
65 def decrypt(self, payload):
66 return self.unpad(self.cipher.decrypt(payload))
67
68 def encrypt(self, payload):
69 return self.cipher.encrypt(self.pad(payload))
70
71
72class InformPacket(object):
73 """Inform model object
74
75 Holds basic, parsed, inform packet data. Does some interpertation for
76 fields like flags. Can be passed to and from the serialiser. This class
77 only fully supports version 1 of the inform data protocol. Version 0
78 payload parsing is not supported.
79 """
80
81 ENCRYPTED_FLAG = 0x1
82 COMPRESSED_FLAG = 0x2
83
84 def __init__(self):
85 self.magic_number = None
86 self.version = None
87 self.mac_addr = None
88 self.flags = None
89 self.iv = None
90 self.data_version = None
91 self.data_length = None
92 self.raw_payload = None
93 self._used_key = None
94
95 def response_copy(self):
96 """Copy object for use in response
97
98 Generates a deep copy of the object and removes the payload so that it
99 can be used to respond to the station that send this inform request.
100 """
101 new_obj = copy.deepcopy(self)
102 new_obj.raw_payload = None
103 return new_obj
104
105 @staticmethod
106 def _format_mac_addr(mac_bytes):
107 return ":".join([binascii.hexlify(i) for i in mac_bytes])
108
109 def _has_flag(self, flag):
110 return self.flags & flag != 0
111
112 @property
113 def formatted_mac_addr(self):
114 return self._format_mac_addr(self.mac_addr)
115
116 @property
117 def is_encrypted(self):
118 return self._has_flag(self.ENCRYPTED_FLAG)
119
120 @property
121 def is_compressed(self):
122 return self._has_flag(self.COMPRESSED_FLAG)
123
124 @property
125 def payload(self):
126 if self.data_version == 1:
127 return json.loads(self.raw_payload.decode("latin-1"))
128 else:
129 return self.raw_payload
130
131 @payload.setter
132 def payload(self, value):
133 self.raw_payload = json.dumps(value)
134
135
136class InformSerializer(object):
137 """Inform protocol version 1 parser/serializer
138
139 Handles the parsing of the inform binary protocol to python objects and
140 seralization of python objects to inform binary protocol. Handles
141 cryptography and data formats. Compatible only with version 1 of the data
142 format.
143 """
144
145 MASTER_KEY = "ba86f2bbe107c7c57eb5f2690775c712"
146 PROTOCOL_MAGIC = 1414414933
147 MAX_VERSION = 1
148
149 def __init__(self, key=None, key_bag=None):
150 self.key = key
151 self.key_bag = key_bag or {}
152
153 def _decrypt_payload(self, packet):
154 i = 0
155 key = self.key_bag.get(packet.formatted_mac_addr)
156
157 for key in (key, self.key, self.MASTER_KEY):
158 if key is None:
159 continue
160
161 decrypted = Cryptor(key, packet.iv).decrypt(packet.raw_payload)
162
163 json.loads(decrypted.decode("latin-1"))
164 packet.raw_payload = decrypted
165 packet._used_key = key
166 break
167
168 def parse(self, input):
169 input_stream = BinaryDataStream(input)
170
171 packet = InformPacket()
172
173 packet.magic_number = input_stream.read_int()
174 assert packet.magic_number == self.PROTOCOL_MAGIC
175
176 packet.version = input_stream.read_int()
177 assert packet.version < self.MAX_VERSION
178
179 packet.mac_addr = input_stream.read_string(6)
180 packet.flags = input_stream.read_short()
181 packet.iv = input_stream.read_string(16)
182 packet.data_version = input_stream.read_int()
183 packet.data_length = input_stream.read_int()
184
185 packet.raw_payload = input_stream.read_string(packet.data_length)
186
187 if packet.is_encrypted:
188 self._decrypt_payload(packet)
189
190 return packet
191
192 def _encrypt_payload(self, packet):
193 if packet.data_version != 1:
194 raise ValueError("Can no encrypt contents of pre 1.0 packets")
195
196 key = packet._used_key if packet._used_key else self.MASTER_KEY
197 return Cryptor(key, packet.iv).encrypt(json.dumps(packet.payload))
198
199 def serialize(self, packet):
200 output = BinaryDataStream.for_output()
201
202 output.write_int(packet.magic_number)
203 output.write_int(packet.version)
204 output.write_string(packet.mac_addr)
205 output.write_short(packet.flags)
206 output.write_string(packet.iv)
207 output.write_int(packet.data_version)
208 output.write_int(packet.data_length)
209 output.write_string(self._encrypt_payload(packet))
210
211 return output.get_output()