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