]> sigrok.org Git - sigrok-util.git/blame - debug/saleae-logicpro16/saleae-logicpro16-dissector.lua
saleae-logicpro16: Add wireshark dissector
[sigrok-util.git] / debug / saleae-logicpro16 / saleae-logicpro16-dissector.lua
CommitLineData
00b7a9ee
JL
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.
27p_logicpro16 = Proto("LogicPro16", "Saleae Logic Pro 16 USB Protocol")
28
29local ctrl_enum = {
30 [0x00] = "Normal",
31 [0x20] = "Reseed",
32}
33
34local 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
48local 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
58local i2c_result_enum = {
59 [0x01] = "Error",
60 [0x02] = "OK",
61}
62
63local crypt_cmd_enum = {
64 [0x00] = "Reset",
65 [0x01] = "Sleep",
66 [0x02] = "Idle",
67 [0x03] = "Normal",
68}
69
70local crypt_op_enum = {
71 [0x16] = "Nonce",
72 [0x1b] = "Random",
73 [0x41] = "Sign",
74}
75
76-- Create the fields exhibited by the protocol.
77p_logicpro16.fields.req = ProtoField.new("Request Frame", "logicpro16.req", ftypes.FRAMENUM)
78p_logicpro16.fields.rsp = ProtoField.new("Response Frame", "logicpro16.rsp", ftypes.FRAMENUM)
79
80p_logicpro16.fields.ctrl = ProtoField.uint8("logicpro16.ctrl", "Control Byte", base.HEX, ctrl_enum)
81p_logicpro16.fields.cmd = ProtoField.uint8("logicpro16.cmd", "Command Byte", base.HEX, cmd_enum)
82p_logicpro16.fields.size = ProtoField.uint16("logicpro16.size", "Payload Size")
83p_logicpro16.fields.unknown = ProtoField.bytes("logicpro16.unknown", "Unidentified message data")
84
85p_logicpro16.fields.eepromaddr = ProtoField.uint16("logicpro16.eepromaddr", "EEPROM Address", base.HEX_DEC)
86p_logicpro16.fields.eepromsize = ProtoField.uint16("logicpro16.eepromsize", "EEPROM Size", base.HEX_DEC)
87
88p_logicpro16.fields.regaddr = ProtoField.uint8("logicpro16.regaddr", "Register Address", base.HEX_DEC, reg_enum)
89p_logicpro16.fields.regval = ProtoField.uint8("logicpro16.regval", "Register Value", base.HEX_DEC)
90
91p_logicpro16.fields.i2caddr = ProtoField.uint8("logicpro16.i2caddr", "I2C Address", base.HEX_DEC)
92p_logicpro16.fields.i2csize = ProtoField.uint16("logicpro16.i2csize", "I2C Size", base.HEX_DEC)
93p_logicpro16.fields.i2cdata = ProtoField.bytes("logicpro16.i2cdata", "I2C Data")
94p_logicpro16.fields.i2cresult = ProtoField.uint8("logicpro16.i2cresult", "I2C Result", base.HEX_DEC, i2c_result_enum)
95
96p_logicpro16.fields.cryptcmd = ProtoField.uint8("logicpro16.cryptcmd", "Crypt Command", base.HEX_DEC, crypt_cmd_enum)
97p_logicpro16.fields.cryptcount = ProtoField.uint8("logicpro16.cryptcount", "Crypt Count", base.HEX_DEC)
98p_logicpro16.fields.cryptop = ProtoField.uint8("logicpro16.cryptop", "Crypt Op", base.HEX_DEC, crypt_op_enum)
99p_logicpro16.fields.cryptp1 = ProtoField.uint8("logicpro16.cryptp1", "Crypt Param 1", base.HEX_DEC)
100p_logicpro16.fields.cryptp2 = ProtoField.uint16("logicpro16.cryptp2", "Crypt Param 2", base.HEX_DEC)
101p_logicpro16.fields.cryptdata = ProtoField.bytes("logicpro16.cryptdata", "Crypt Data")
102p_logicpro16.fields.cryptcrc = ProtoField.uint16("logicpro16.cryptcrc", "Crypt CRC", base.HEX_DEC)
103
104p_logicpro16.fields.rawdata = ProtoField.bytes("logicpro16.rawdata", "Raw Message Data")
105p_logicpro16.fields.decrypted = ProtoField.bytes("logicpro16.decrypted", "Decrypted message data")
106
107-- Referenced USB URB dissector fields.
108local f_urb_type = Field.new("usb.urb_type")
109local f_transfer_type = Field.new("usb.transfer_type")
110local f_endpoint = Field.new("usb.endpoint_number.endpoint")
111local f_direction = Field.new("usb.endpoint_number.direction")
112
113local iv = 0x354B248E
114local state = iv
115local states = {}
116
117local request_frame
118local response_requests = {}
119local request_responses = {}
120
121local request_cmd
122local response_cmds = {}
123
124local crypt_op
125local crypt_nonce_in
126local crypt_nonce_out
127local crypt_sign_out_crc
128
129local 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
147end
148
149local 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
167end
168
169-- Decrypt EP1 OUT message
170local 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
211end
212
213-- Decrypt EP1 IN message
214local 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")
237end
238
239-- Dissect control command messages.
240local 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()
306end
307
308-- Dissect control response messages.
309local 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()
358end
359
360-- Main dissector function.
361function 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
398end
399
400-- Register Logic Pro 16 protocol dissector during initialization.
401function 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)
421end