diff options
Diffstat (limited to 'python/inform.py')
-rw-r--r-- | python/inform.py | 211 |
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 @@ | |||
1 | import json | ||
2 | import copy | ||
3 | import struct | ||
4 | import binascii | ||
5 | from Crypto.Cipher import AES | ||
6 | from cStringIO import StringIO | ||
7 | |||
8 | |||
9 | class 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 | |||
45 | class 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 | |||
72 | class 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 | |||
136 | class 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() | ||