]> sigrok.org Git - sigrok-util.git/blob - debug/saleae-logicpro16/saleae-logicpro16-dissector.lua
saleae-logicpro16: Add wireshark dissector
[sigrok-util.git] / debug / saleae-logicpro16 / saleae-logicpro16-dissector.lua
1 -- Logic Pro 16 protocol dissector for Wireshark
2 --
3 -- Copyright (C) 2016-2017 Jan Luebbe <jluebbe@lasnet.de>
4 --
5 -- based on the LWLA dissector, which is
6 --   Copyright (C) 2015 Stefan Bruens <stefan.bruens@rwth-aachen.de>
7 --
8 -- based on the LWLA dissector, which is
9 --   Copyright (C) 2014 Daniel Elstner <daniel.kitta@gmail.com>
10 --
11 -- This program is free software; you can redistribute it and/or modify
12 -- it under the terms of the GNU General Public License as published by
13 -- the Free Software Foundation; either version 3 of the License, or
14 -- (at your option) any later version.
15 --
16 -- This program is distributed in the hope that it will be useful,
17 -- but WITHOUT ANY WARRANTY; without even the implied warranty of
18 -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 -- GNU General Public License for more details.
20 --
21 -- You should have received a copy of the GNU General Public License
22 -- along with this program; if not, see <http://www.gnu.org/licenses/>.
23
24 -- Usage: wireshark -X lua_script:saleae-logicpro16-dissector.lua
25 --
26 -- Create custom protocol for the Saleae Logic Pro 16 analyzer.
27 p_logicpro16 = Proto("LogicPro16", "Saleae Logic Pro 16 USB Protocol")
28
29 local ctrl_enum = {
30     [0x00] = "Normal",
31     [0x20] = "Reseed",
32 }
33
34 local cmd_enum = {
35     [0x01] = "Start Capture",
36     [0x02] = "Stop Capture",
37     [0x07] = "Read EEPROM",
38     [0x7e] = "Prepare Firmware Upload",
39     [0x7f] = "Upload Firmware",
40     [0x80] = "Write Register",
41     [0x81] = "Read Register",
42     [0x87] = "Write I2C",
43     [0x88] = "Read I2C",
44     [0x89] = "Wake I2C",
45     [0x8b] = "Read Firmware Version",
46 }
47
48 local reg_enum = {
49     [0x08] = "Analog Channels (LSB)",
50     [0x09] = "Analog Channels (MSB)",
51     [0x06] = "Digital Channels (LSB)",
52     [0x07] = "Digital Channels (MSB)",
53     [0x0f] = "LED Red",
54     [0x10] = "LED Green",
55     [0x11] = "LED Blue",
56 }
57
58 local i2c_result_enum = {
59     [0x01] = "Error",
60     [0x02] = "OK",
61 }
62
63 local crypt_cmd_enum = {
64     [0x00] = "Reset",
65     [0x01] = "Sleep",
66     [0x02] = "Idle",
67     [0x03] = "Normal",
68 }
69
70 local crypt_op_enum = {
71     [0x16] = "Nonce",
72     [0x1b] = "Random",
73     [0x41] = "Sign",
74 }
75
76 -- Create the fields exhibited by the protocol.
77 p_logicpro16.fields.req  = ProtoField.new("Request Frame", "logicpro16.req", ftypes.FRAMENUM)
78 p_logicpro16.fields.rsp  = ProtoField.new("Response Frame", "logicpro16.rsp", ftypes.FRAMENUM)
79
80 p_logicpro16.fields.ctrl  = ProtoField.uint8("logicpro16.ctrl", "Control Byte", base.HEX, ctrl_enum)
81 p_logicpro16.fields.cmd  = ProtoField.uint8("logicpro16.cmd", "Command Byte", base.HEX, cmd_enum)
82 p_logicpro16.fields.size  = ProtoField.uint16("logicpro16.size", "Payload Size")
83 p_logicpro16.fields.unknown  = ProtoField.bytes("logicpro16.unknown", "Unidentified message data")
84
85 p_logicpro16.fields.eepromaddr  = ProtoField.uint16("logicpro16.eepromaddr", "EEPROM Address", base.HEX_DEC)
86 p_logicpro16.fields.eepromsize  = ProtoField.uint16("logicpro16.eepromsize", "EEPROM Size", base.HEX_DEC)
87
88 p_logicpro16.fields.regaddr = ProtoField.uint8("logicpro16.regaddr", "Register Address", base.HEX_DEC, reg_enum)
89 p_logicpro16.fields.regval  = ProtoField.uint8("logicpro16.regval", "Register Value", base.HEX_DEC)
90
91 p_logicpro16.fields.i2caddr  = ProtoField.uint8("logicpro16.i2caddr", "I2C Address", base.HEX_DEC)
92 p_logicpro16.fields.i2csize  = ProtoField.uint16("logicpro16.i2csize", "I2C Size", base.HEX_DEC)
93 p_logicpro16.fields.i2cdata  = ProtoField.bytes("logicpro16.i2cdata", "I2C Data")
94 p_logicpro16.fields.i2cresult  = ProtoField.uint8("logicpro16.i2cresult", "I2C Result", base.HEX_DEC, i2c_result_enum)
95
96 p_logicpro16.fields.cryptcmd  = ProtoField.uint8("logicpro16.cryptcmd", "Crypt Command", base.HEX_DEC, crypt_cmd_enum)
97 p_logicpro16.fields.cryptcount  = ProtoField.uint8("logicpro16.cryptcount", "Crypt Count", base.HEX_DEC)
98 p_logicpro16.fields.cryptop  = ProtoField.uint8("logicpro16.cryptop", "Crypt Op", base.HEX_DEC, crypt_op_enum)
99 p_logicpro16.fields.cryptp1  = ProtoField.uint8("logicpro16.cryptp1", "Crypt Param 1", base.HEX_DEC)
100 p_logicpro16.fields.cryptp2  = ProtoField.uint16("logicpro16.cryptp2", "Crypt Param 2", base.HEX_DEC)
101 p_logicpro16.fields.cryptdata  = ProtoField.bytes("logicpro16.cryptdata", "Crypt Data")
102 p_logicpro16.fields.cryptcrc  = ProtoField.uint16("logicpro16.cryptcrc", "Crypt CRC", base.HEX_DEC)
103
104 p_logicpro16.fields.rawdata   = ProtoField.bytes("logicpro16.rawdata", "Raw Message Data")
105 p_logicpro16.fields.decrypted = ProtoField.bytes("logicpro16.decrypted", "Decrypted message data")
106
107 -- Referenced USB URB dissector fields.
108 local f_urb_type = Field.new("usb.urb_type")
109 local f_transfer_type = Field.new("usb.transfer_type")
110 local f_endpoint = Field.new("usb.endpoint_number.endpoint")
111 local f_direction = Field.new("usb.endpoint_number.direction")
112
113 local iv = 0x354B248E
114 local state = iv
115 local states = {}
116
117 local request_frame
118 local response_requests = {}
119 local request_responses = {}
120
121 local request_cmd
122 local response_cmds = {}
123
124 local crypt_op
125 local crypt_nonce_in
126 local crypt_nonce_out
127 local crypt_sign_out_crc
128
129 local function iterate_state()
130     local max = bit32.band(state, 0x1f) + 34
131     --print(string.format("shift -/%i state 0x%x", max, state))
132     for i = 0, max, 1 do
133         state = bit32.bor(
134             bit32.rshift(state, 1),
135             bit32.lshift(
136                 bit32.bxor(
137                     state,
138                     bit32.rshift(state, 1),
139                     bit32.rshift(state, 21),
140                     bit32.rshift(state, 31)
141                 ),
142                 31
143             )
144         )
145         --print(string.format("shift %i/%i state 0x%x", i, max, state))
146      end
147 end
148
149 local function reinit_state()
150     print("in", crypt_nonce_in)
151     print("out", crypt_nonce_out)
152     print("crc", crypt_sign_out_crc)
153     local input = crypt_nonce_in(4, 16) .. crypt_nonce_out(0, 28) .. crypt_sign_out_crc .. ByteArray.new("00000000") -- add padding
154     print("input", input)
155     local result = 0
156     for i = 0, input:len()-4, 4 do
157         result = bit32.bxor(
158             result,
159             input:get_index(i),
160             bit32.lshift(input:get_index(i+1), 8),
161             bit32.lshift(input:get_index(i+2), 16),
162             bit32.lshift(input:get_index(i+3), 24)
163         )
164         print(string.format("iterate 0x%x 0x%x 0x%x 0x%x 0x%x", result, input:get_index(i), input:get_index(i+1), input:get_index(i+2), input:get_index(i+3)))
165     end
166     state = result
167 end
168
169 -- Decrypt EP1 OUT message
170 local function decrypt_ep1_out_message(pinfo, range)
171     local out = ByteArray.new()
172     out:set_size(range:len())
173  
174     local ctrl = range(0, 1):uint()
175     local reseed = bit32.btest(ctrl, 0x20)
176     if not pinfo.visited then
177         if reseed then
178             state = iv
179         end
180         states[pinfo.number] = state
181     end
182  
183     local secret = states[pinfo.number]
184     print("out visited", pinfo.visited, string.format("secret 0x%x", secret))
185  
186     for i = 0, range:len() - 1, 1 do
187         local value = range(i,1):uint()
188         local mask = bit32.extract(secret, 8*(i%4), 8)
189         local dec
190         if i == 0 then
191             -- only 0x20 and 0x08 are relevant here
192             dec = bit32.band(value, 0x28)
193         else
194             dec = bit32.bxor(value, mask)
195         end
196         out:set_index(i, dec)
197     end
198  
199     tvb = ByteArray.tvb(out, "Decrypted")
200  
201     if not pinfo.visited then
202         if reseed then
203             state = tvb:range(1,4):le_uint()
204             print("reseed", string.format("state 0x%x", secret))
205         else
206             iterate_state()
207         end
208     end
209  
210     return tvb
211 end
212
213 -- Decrypt EP1 IN message
214 local function decrypt_ep1_in_message(pinfo, range)
215     local out = ByteArray.new()
216     out:set_size(range:len())
217
218     if not pinfo.visited then
219        states[pinfo.number] = state
220     end
221
222     local secret = states[pinfo.number]
223     print("in visited", pinfo.visited, string.format("secret 0x%x", secret))
224  
225     for i = 0, range:len() - 1, 1 do
226         local value = range(i,1):uint()
227         local mask = bit32.extract(secret, 8*(i%4), 8)
228         local dec = bit32.bxor(value, mask)
229         out:set_index(i, dec)
230     end
231  
232     if not pinfo.visited then
233         iterate_state()
234     end
235  
236     return ByteArray.tvb(out, "Decrypted")
237 end
238
239 -- Dissect control command messages.
240 local function dissect_command(range, pinfo, tree)
241
242     tree:add(p_logicpro16.fields.ctrl, range(0, 1))
243     tree:add(p_logicpro16.fields.cmd, range(1, 1))
244     local cmd = range(1, 1):uint()
245     request_cmd = cmd
246     request_frame = pinfo.number
247     response_frame = request_responses[pinfo.number]
248
249     pinfo.cols.info = string.format("command:  0x%02x %s",
250                                     cmd, tostring(range))
251
252     if not (response_frame == nil) then
253         tree:add(p_logicpro16.fields.rsp, response_frame):set_generated()
254     end
255
256     if cmd == 0x7 then -- eeprom read
257         tree:add(p_logicpro16.fields.unknown, range(2, 2))
258         tree:add_le(p_logicpro16.fields.eepromaddr, range(4, 2))
259         tree:add_le(p_logicpro16.fields.eepromsize, range(6, 2))
260     elseif cmd == 0x7f then -- firmware upload
261         tree:add_le(p_logicpro16.fields.size, range(2, 2))
262         tree:add(p_logicpro16.fields.rawdata, range(4))
263     elseif cmd == 0x80 then -- register write
264         tree:add(p_logicpro16.fields.size, range(2, 1))
265         local t = tree:add(p_logicpro16.fields.rawdata, range(3))
266         for i = 0, range(2, 1):uint() - 1, 1 do
267             t:add(p_logicpro16.fields.regaddr, range(3+i*2, 1))
268             t:add(p_logicpro16.fields.regval, range(3+i*2+1, 1))
269         end
270     elseif cmd == 0x81 then -- register read
271         tree:add(p_logicpro16.fields.size, range(2, 1))
272         local t = tree:add(p_logicpro16.fields.rawdata, range(3))
273         for i = 0, range(2, 1):uint() - 1, 1 do
274             t:add(p_logicpro16.fields.regaddr, range(3+i, 1))
275         end
276     elseif cmd == 0x87 then -- i2c write
277         tree:add(p_logicpro16.fields.i2caddr, range(2, 1))
278         tree:add_le(p_logicpro16.fields.i2csize, range(3, 2))
279         local t = tree:add(p_logicpro16.fields.i2cdata, range(5))
280         t:add(p_logicpro16.fields.cryptcmd, range(5, 1))
281         local cryptcmd = range(5, 1):uint()
282         if cryptcmd == 0x03 then
283             t:add(p_logicpro16.fields.cryptcount, range(6, 1))
284             local cryptdatalen = range(6, 1):uint() - 7
285             t:add(p_logicpro16.fields.cryptop, range(7, 1))
286             crypt_op = range(7, 1):uint()
287             t:add(p_logicpro16.fields.cryptp1, range(8, 1))
288             t:add(p_logicpro16.fields.cryptp2, range(9, 2))
289             if cryptdatalen > 0 then
290                 t:add(p_logicpro16.fields.cryptdata, range(11, cryptdatalen))
291             end
292             t:add(p_logicpro16.fields.cryptcrc, range(11+cryptdatalen, 2))
293             if crypt_op == 0x16 then -- nonce
294                 crypt_nonce_in = range(11, cryptdatalen):bytes()
295             end
296         end
297     elseif cmd == 0x88 then -- i2c read
298         tree:add(p_logicpro16.fields.i2caddr, range(2, 1))
299         tree:add_le(p_logicpro16.fields.i2csize, range(3, 2))
300     elseif cmd == 0x89 then -- i2c wake
301     else
302         local item = tree:add(p_logicpro16.fields.unknown, range(2))
303         item:add_expert_info(PI_UNDECODED, PI_WARN, "Leftover data")
304     end
305     return range:len()
306 end
307
308 -- Dissect control response messages.
309 local function dissect_response(range, pinfo, tree)
310     local cmd
311     if not pinfo.visited then
312         response_requests[pinfo.number] = request_frame
313         response_cmds[pinfo.number] = request_cmd
314         request_responses[request_frame] = pinfo.number
315     else
316         request_frame = response_requests[pinfo.number]
317         request_cmd = response_cmds[pinfo.number]
318     end
319
320     print("visited", pinfo.visited, string.format("request_cmd 0x%x", request_cmd))
321     pinfo.cols.info = string.format("response: 0x%02x %s",
322                                     request_cmd, tostring(range))
323
324     tree:add(p_logicpro16.fields.req, request_frame):set_generated()
325     tree:add(p_logicpro16.fields.cmd, request_cmd):set_generated()
326
327     if request_cmd == 0x81 then -- register read
328         local t = tree:add(p_logicpro16.fields.rawdata, range(1))
329     elseif request_cmd == 0x87 then -- i2c write
330         tree:add(p_logicpro16.fields.i2cresult, range(0, 1))
331         local t = tree:add(p_logicpro16.fields.i2cdata, range(1))
332     elseif request_cmd == 0x88 then -- i2c read
333         tree:add(p_logicpro16.fields.i2cresult, range(0, 1))
334         local i2cresult = range(0, 1):uint()
335         local t = tree:add(p_logicpro16.fields.i2cdata, range(1))
336         if i2cresult == 0x02 then
337             t:add(p_logicpro16.fields.cryptcount, range(1, 1))
338             local cryptdatalen = range(1, 1):uint() - 3
339             if cryptdatalen > 0 then
340                 t:add(p_logicpro16.fields.cryptdata, range(2, cryptdatalen))
341             end
342             t:add(p_logicpro16.fields.cryptcrc, range(2+cryptdatalen, 2))
343             if crypt_op == 0x16 then -- nonce
344                 crypt_nonce_out = range(2, cryptdatalen):bytes()
345             elseif crypt_op == 0x41 then -- sign
346                 crypt_sign_out_crc = range(2+cryptdatalen, 2):bytes()
347                 if not pinfo.visited then
348                     reinit_state()
349                 end
350             end
351             crypt_op = nil
352         end
353     else
354         local item = tree:add(p_logicpro16.fields.unknown, range())
355         item:add_expert_info(PI_UNDECODED, PI_WARN, "Leftover data")
356     end
357     return range:len()
358 end
359
360 -- Main dissector function.
361 function p_logicpro16.dissector(tvb, pinfo, tree)
362     local transfer_type = tonumber(tostring(f_transfer_type()))
363
364     -- Bulk transfers only.
365     if transfer_type == 3 then
366         local urb_type = tonumber(tostring(f_urb_type()))
367         local endpoint = tonumber(tostring(f_endpoint()))
368         local direction = tonumber(tostring(f_direction()))
369
370         -- Payload-carrying packets only.
371         if (urb_type == 83 and endpoint == 1)   -- 'S' - Submit
372             or (urb_type == 67 and endpoint == 1) -- 'C' - Complete
373         then
374             pinfo.cols.protocol = p_logicpro16.name
375
376             local subtree = tree:add(p_logicpro16, tvb(), "Logic Pro 16")
377             subtree:add(p_logicpro16.fields.rawdata, tvb())
378
379             local dec
380             if (direction == 0) then
381                dec = decrypt_ep1_out_message(pinfo, tvb)
382             else
383                dec = decrypt_ep1_in_message(pinfo, tvb)
384             end
385
386             local dectree = subtree:add(p_logicpro16.fields.decrypted, dec())
387             dectree:set_generated()
388
389             -- Dispatch to message-specific dissection handler.
390             if (direction == 0) then
391                 return dissect_command(dec, pinfo, dectree)
392             else
393                 return dissect_response(dec, pinfo, dectree)
394             end
395         end
396     end
397     return 0
398 end
399
400 -- Register Logic Pro 16 protocol dissector during initialization.
401 function p_logicpro16.init()
402     local usb_product_dissectors = DissectorTable.get("usb.product")
403
404     -- Dissection by vendor+product ID requires that Wireshark can get the
405     -- the device descriptor.  Making a USB device available inside a VM
406     -- will make it inaccessible from Linux, so Wireshark cannot fetch the
407     -- descriptor by itself.  However, it is sufficient if the guest requests
408     -- the descriptor once while Wireshark is capturing.
409     usb_product_dissectors:add(0x21a91006, p_logicpro16)
410
411     -- Addendum: Protocol registration based on product ID does not always
412     -- work as desired.  Register the protocol on the interface class instead.
413     -- The downside is that it would be a bad idea to put this into the global
414     -- configuration, so one has to make do with -X lua_script: for now.
415     -- local usb_bulk_dissectors = DissectorTable.get("usb.bulk")
416
417     -- For some reason the "unknown" class ID is sometimes 0xFF and sometimes
418     -- 0xFFFF.  Register both to make it work all the time.
419     -- usb_bulk_dissectors:add(0xFF, p_logicpro16)
420     -- usb_bulk_dissectors:add(0xFFFF, p_logicpro16)
421 end