├── README.md ├── crc.py └── test.lua /README.md: -------------------------------------------------------------------------------- 1 | # Dump 2 | 3 | brain dump for reverse engineering garmin's protocol for vivofit3 4 | 5 | end goal: set the time 6 | 7 | SEE: https://github.com/mjsir911/Gadgetbridge 8 | 9 | 10 | To use `test.lua`, run `wireshark` with `-X lua_script:test.lua` 11 | -------------------------------------------------------------------------------- /crc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # vim: set fileencoding=utf-8 : 3 | 4 | __appname__ = "crc" 5 | __author__ = "@AUTHOR@" 6 | __copyright__ = "" 7 | __credits__ = ["@AUTHOR@"] # Authors and bug reporters 8 | __license__ = "GPL" 9 | __version__ = "1.0" 10 | __maintainers__ = "@AUTHOR@" 11 | __email__ = "@EMAIL@" 12 | __status__ = "Prototype" # "Prototype", "Development" or "Production" 13 | __module__ = "" 14 | 15 | 16 | def crc(barray) -> int: 17 | """ 18 | this is CRC-16-ANSI (0x8005) 19 | >>> hex(crc([0x07, 0x00, 0x8f, 0x13, 0x03])) 20 | '0x1ac9' 21 | 22 | >>> hex(crc([0x06, 0x00, 0xa7, 0x13])) 23 | '0x753b' 24 | """ 25 | magic = [0, 52225, 55297, 5120, 61441, 15360, 10240, 58369, 40961, 27648, 30720, 46081, 20480, 39937, 34817, 17408] 26 | 27 | def nbytes(i, n): 28 | return i & ((1 << (n * 4)) - 1) 29 | 30 | def helper(i, b): 31 | i = nbytes(i >> 4, 3) ^ magic[nbytes(i, 1)] 32 | i ^= magic[nbytes(b, 1)] 33 | i = nbytes(i >> 4, 3) ^ magic[nbytes(i, 1)] 34 | i ^= magic[nbytes(b >> 4, 1)] 35 | return i 36 | 37 | acc = 0 38 | for b in barray: 39 | acc = helper(acc, b) 40 | return acc 41 | 42 | 43 | import doctest 44 | doctest.testmod() 45 | -------------------------------------------------------------------------------- /test.lua: -------------------------------------------------------------------------------- 1 | service_uuid_f = Field.new("btatt.service_uuid128") 2 | btatt_handle_f = Field.new("btatt.handle") 3 | btatt_value_f = Field.new("btatt.value") 4 | 5 | 6 | 7 | 8 | vivofit_proto = Proto("vivofit", "Vivofit communications protocol") 9 | vivofit_table = DissectorTable.new("vivofit.type", nil, ftypes.UINT16, base.HEX, vivofit_proto) 10 | 11 | message_length = ProtoField.uint16("vivofit.length", "Message Length", base.DEC) 12 | message_type = ProtoField.uint16("vivofit.type", "Message Type", base.HEX) 13 | message_payload = ProtoField.bytes("vivofit.payload", "Message Payload") 14 | message_checksum = ProtoField.uint16("vivofit.checksum", "Checksum", base.HEX) 15 | message_ack = ProtoField.framenum("vivofit.ack_frame", "Acknowledgment Frame") 16 | 17 | fragment_frames = ProtoField.framenum("vivofit.fragment", "Fragment of Packet") 18 | fragment_reassembled = ProtoField.framenum("vivofit.reassembed_in", "Reassembly in") 19 | 20 | vivofit_proto.fields = { 21 | message_length, 22 | message_type, 23 | message_payload, 24 | message_checksum, 25 | message_ack, 26 | fragment_frames, 27 | fragment_reassembled, 28 | } 29 | function vivofit_proto.init() 30 | cur_fragments = {} 31 | cached_reassembled = {} 32 | 33 | reply_table = {} 34 | ireply_frame_cache = {} 35 | end 36 | 37 | function deCOBS(buf) 38 | local nbuf = buf:bytes() 39 | assert(buf(0, 1):uint() == 0) 40 | local next_zero = 1 41 | while (nbuf:get_index(next_zero) ~= 0) 42 | do 43 | local cur_zero = next_zero 44 | next_zero = next_zero + nbuf:get_index(next_zero) 45 | nbuf:set_index(cur_zero, 0) 46 | end 47 | return ByteArray.tvb(nbuf:subset(2, nbuf:len() - 3), "de-cobs") 48 | end 49 | 50 | function vivofit_proto.dissector(buffer, pinfo, root) 51 | print(btatt_handle_f()); 52 | if btatt_handle_f() == nil then return end 53 | if tostring(btatt_handle_f()) ~= "0x0000000e" and tostring(btatt_handle_f()) ~= "0x00000010" then return end 54 | -- if service_uuid_f() == nil then return end 55 | -- if tostring(service_uuid_f()) ~= "9b:01:24:01:bc:30:ce:9a:e1:11:0f:67:e4:91:ab:de" then return end 56 | if btatt_value_f() == nil then return end 57 | buffer = btatt_value_f().range 58 | 59 | local last_packet = buffer(buffer:len() - 1):int() == 0 60 | 61 | frag_cache = cached_reassembled[pinfo.number] or {} 62 | 63 | if not pinfo.visited then 64 | if cur_fragments[tostring(pinfo.src)] == nil then 65 | cur_fragments[tostring(pinfo.src)] = {} 66 | end 67 | local frag_group = cur_fragments[tostring(pinfo.src)] 68 | frag_group[#frag_group + 1] = {num = pinfo.number, buf = buffer:bytes()} 69 | if last_packet then 70 | local abuf = ByteArray.new() 71 | frag_cache.buf = abuf 72 | 73 | local frames = {} 74 | frag_cache.frames = frames 75 | 76 | for i = 1,#frag_group,1 do 77 | frames[i] = frag_group[i].num 78 | abuf:append(frag_group[i].buf) 79 | cached_reassembled[frag_group[i].num] = frag_cache 80 | end 81 | cur_fragments[tostring(pinfo.src)] = {} 82 | end 83 | else 84 | if frag_cache.frames ~= nil and not last_packet then 85 | local last_frame = frag_cache.frames[#frag_cache.frames] 86 | root:add(fragment_reassembled, last_frame) 87 | :set_generated(true) 88 | end 89 | end 90 | 91 | 92 | if not last_packet then return end 93 | 94 | pinfo.cols.protocol = vivofit_proto.name 95 | local defragged = frag_cache.buf:tvb("Reassembled") 96 | buffer = deCOBS(defragged) 97 | local tree = root:add(vivofit_proto, buffer(), "VivoFit Protocol Data") 98 | local reassembled = tree:add(buffer(0), "Reassembled Segments") 99 | :set_generated(true) 100 | for i = 1,#frag_cache.frames,1 do 101 | local buf_offset = (i - 1) * 20 102 | local rest = math.min(defragged:len(), buf_offset + 20) - buf_offset 103 | reassembled:add(fragment_frames, defragged(buf_offset, rest), frag_cache.frames[i]) 104 | :set_generated(true) 105 | end 106 | tree:add_le(message_length, buffer(0,2)) 107 | tree:add_le(message_type, buffer(2,2)) 108 | tree:add_le(message_checksum, buffer(buffer:len() - 2, 2)) 109 | 110 | if not pinfo.visited then 111 | reply_table[buffer(2, 2):le_uint()] = pinfo.number 112 | end 113 | 114 | if ireply_frame_cache[pinfo.number] ~= nil then 115 | tree:add(message_ack, ireply_frame_cache[pinfo.number]) 116 | :set_generated(true) 117 | end 118 | 119 | tree:add(message_payload, buffer(4, buffer:len() - 6)) 120 | pinfo.cols.info = "Message Type: 0x" .. buffer(3, 1) .. buffer(2, 1) 121 | vivofit_table:try(buffer(2,2):le_uint(), buffer(4, buffer:len() - 6):tvb(), pinfo, root) 122 | end 123 | 124 | register_postdissector(vivofit_proto) 125 | -- 126 | -- 127 | -- Begin Ack 0x1388 128 | -- 129 | vivofit_ack_table = DissectorTable.new("vivofit.ack.type", nil, ftypes.UINT16, base.HEX, vivofit_proto) 130 | vivofit_ack = Proto("vivofit.ack", "Vivofit Acknowledgment") 131 | ack_reply_type = ProtoField.uint16("vivofit.ack.type", "Reply Type", base.HEX) 132 | ack_reply_frame = ProtoField.framenum("vivofit.ack.reply", "Reply Frame", base.NONE, frametype.ACK) 133 | vivofit_ack.fields = { 134 | ack_reply_type, 135 | ack_reply_frame, 136 | } 137 | function vivofit_ack.init() 138 | reply_frame_cache = {} 139 | end 140 | function vivofit_ack.dissector(buffer, pinfo, root) 141 | local tree = root:add(vivofit_ack, buffer()) 142 | tree:add_le(ack_reply_type, buffer(0, 2)) 143 | pinfo.cols.info = "Message reply to: 0x" .. buffer(1, 1) .. buffer(0, 1) 144 | if not pinfo.visited then 145 | reply_frame_cache[pinfo.number] = reply_table[buffer(0, 2):le_uint()] 146 | ireply_frame_cache[reply_table[buffer(0, 2):le_uint()]] = pinfo.number 147 | end 148 | tree:add_le(ack_reply_frame, reply_frame_cache[pinfo.number]) 149 | :set_generated(true) 150 | local type = buffer(0, 2):le_uint() 151 | if vivofit_ack_table:get_dissector(type) ~= nil then 152 | vivofit_ack_table:try(type, buffer(3):tvb(), pinfo, root) 153 | else 154 | vivofit_table:try(type, buffer(3):tvb(), pinfo, root) 155 | end 156 | end 157 | vivofit_table:add(0x1388, vivofit_ack) 158 | -- 159 | -- 160 | -- Begin System Message 0x13a6 161 | -- 162 | vivofit_system_event = Proto("vivofit.system_event", "Vivofit System Event") 163 | system_event_type = ProtoField.uint8("vivofit.system_event.type", "Type") 164 | vivofit_system_event.fields = { 165 | system_event_type, 166 | } 167 | function vivofit_system_event.dissector(buffer, pinfo, root) 168 | local tree = root:add(vivofit_system_event, buffer()) 169 | tree:add_le(system_event_type, buffer(0, 1)) 170 | pinfo.cols.info = "System Event" 171 | end 172 | vivofit_table:add(0x13a6, vivofit_system_event) 173 | -- 174 | -- 175 | -- Begin Set Time 0x13a2 176 | -- 177 | vivofit_set_time = Proto("vivofit.set_time", "Vivofit Set Time Message") 178 | time_settings_count = ProtoField.uint8("vivofit.set_time.count", "Time Settings Count", base.DEC) 179 | time_setting = ProtoField.protocol("vivofit.set_time.setting", "Time Setting") 180 | time_setting_len = ProtoField.uint8("vivofit.set_time.setting.len", "Setting Length") 181 | time_setting_type = ProtoField.uint8("vivofit.set_time.setting.type", "Setting Type") 182 | time_setting_data = ProtoField.uint32("vivofit.set_time.setting.data", "Setting Data") 183 | mytest = ProtoField.absolute_time("vivofit_set_time.setting.time", "Setting Time") 184 | vivofit_set_time.fields = { 185 | time_settings_count, 186 | time_setting, 187 | time_setting_len, 188 | time_setting_type, 189 | time_setting_data, 190 | mytest, 191 | } 192 | 193 | time_base = NSTime(631065600) - NSTime() 194 | function vivofit_set_time.dissector(buffer, pinfo, root) 195 | -- print("vivofit_set_time: " .. tostring(pinfo.number)) 196 | local tree = root:add(vivofit_set_time, buffer()) 197 | tree:add(time_settings_count, buffer(0, 1)) 198 | local i = 1 199 | for _ = 1,buffer(0, 1):uint(),1 do 200 | local start = i 201 | type = buffer(i, 1) 202 | i = i + 1 203 | len = buffer(i, 1) 204 | i = i + 1 205 | data = buffer(i, len:uint()) 206 | i = i + len:uint() 207 | local subtree = tree:add(time_setting, buffer(start, len:uint() + 2), "test") 208 | subtree:add_le(time_setting_type, type) 209 | subtree:add_le(time_setting_len, len) 210 | subtree:add_le(mytest, data, NSTime(data:le_uint()) + time_base) 211 | -- print(os.date()) 212 | end 213 | pinfo.cols.info = "Time set message" 214 | end 215 | vivofit_table:add(0x13a2, vivofit_set_time) 216 | -- 217 | -- 218 | -- Begin Device Info 0x13a0 219 | -- 220 | vivofit_device_info = Proto("vivofit.device_info", "VivoFit Device Info") 221 | info_proto_version = ProtoField.uint16("vivofit.device_info.proto_version", "Protocol Version") 222 | info_product_num = ProtoField.uint16("vivofit.device_info.product_number", "Product Number") 223 | info_unit_id = ProtoField.uint32("vivofit.device_info.unit_id", "Unit ID") 224 | info_software_version = ProtoField.uint16("vivofit.device_info.software_version", "Software Version") 225 | info_max_packet_size = ProtoField.uint16("vivofit.device_info.max_packet_size", "Maximum Packet Size", base.HEX) 226 | info_name_length = ProtoField.uint8("vivofit.device_info.name.length", "Device Name Length") 227 | info_name = ProtoField.string("vivofit.device_info.name", "Device Name", base.UTF8) 228 | info_manufacturer_length = ProtoField.uint8("vivofit.device_info.manufacturer.length", "Device Manufacturer Length") 229 | info_manufacturer = ProtoField.string("vivofit.device_info.manufacturer", "Device Manufacturer", base.UTF8) 230 | info_model_length = ProtoField.uint8("vivofit.device_info.model.length", "Device Model Length") 231 | info_model = ProtoField.string("vivofit.device_info.model", "Device Model", base.UTF8) 232 | vivofit_device_info.fields = { 233 | info_proto_version, 234 | info_product_num, 235 | info_unit_id, 236 | info_software_version, 237 | info_max_packet_size, 238 | info_name_length, 239 | info_name, 240 | info_manufacturer_length, 241 | info_manufacturer, 242 | info_model_length, 243 | info_model, 244 | } 245 | function vivofit_device_info.dissector(buffer, pinfo, root) 246 | local tree = root:add(vivofit_device_info, buffer()) 247 | tree:add_le(info_proto_version, buffer(0, 2)) 248 | tree:add_le(info_product_num, buffer(2, 2)) 249 | tree:add_le(info_unit_id, buffer(4, 4)) 250 | tree:add_le(info_software_version, buffer(8, 2)) 251 | tree:add_le(info_max_packet_size, buffer(10, 2)) 252 | 253 | offset = 12 254 | tree:add_le(info_name_length, buffer(offset, 1)) 255 | tree:add_packet_field(info_name, buffer(offset + 1, buffer(offset, 1):uint()), ENC_UTF_8) 256 | 257 | offset = offset + buffer(offset, 1):uint() + 1 258 | tree:add_le(info_manufacturer_length, buffer(offset, 1)) 259 | tree:add_packet_field(info_manufacturer, buffer(offset + 1, buffer(offset, 1):uint()), ENC_UTF_8) 260 | 261 | offset = offset + buffer(offset, 1):uint() + 1 262 | tree:add_le(info_model_length, buffer(offset, 1)) 263 | tree:add_packet_field(info_model, buffer(offset + 1, buffer(offset, 1):uint()), ENC_UTF_8) 264 | pinfo.cols.info = "Device Info" 265 | end 266 | vivofit_table:add(0x13a0, vivofit_device_info) 267 | -- 268 | -- 269 | -- Begin Download Request 0x138a 270 | -- 271 | vivofit_download_request = Proto("vivofit.download_request", "Vivofit Download Request") 272 | download_request_filenum = ProtoField.uint16("vivofit.download_request.filenum", "File Number", base.HEX) 273 | download_request_offset = ProtoField.uint32("vivofit.download_request.offset", "Offset") 274 | download_request_type = ProtoField.uint8("vivofit.download_request.type", "Type", base.HEX) 275 | download_request_data = ProtoField.framenum("vivofit.download_request.data", "Data") 276 | -- download_request_length = ProtoField.uint32("vivofit.download_request.length", "Length") 277 | vivofit_download_request.fields = { 278 | download_request_filenum, 279 | download_request_offset, 280 | download_request_type, 281 | download_request_data, 282 | -- download_request_length, 283 | } 284 | function vivofit_download_request.init() 285 | downloaded_file = nil 286 | -- = { 287 | -- request_frame = num 288 | -- len = num 289 | -- count = num 290 | -- data_frames = [{ 291 | -- buf = buffer 292 | -- num = num 293 | -- len = number 294 | -- }] 295 | cached_downloads = {} 296 | -- = { 297 | -- num1, num2, num3 { 298 | -- buf = buffer 299 | -- frames = [{ num = number, len = number}] 300 | -- } 301 | -- } 302 | end 303 | function vivofit_download_request.dissector(buffer, pinfo, root) 304 | pinfo.cols.info = "Download Request" 305 | local tree = root:add(vivofit_download_request, buffer()) 306 | tree:add_le(download_request_filenum, buffer(0, 2)) 307 | tree:add_le(download_request_offset, buffer(2, 4)) 308 | tree:add_le(download_request_type, buffer(6, 1)) 309 | 310 | if not pinfo.visited then 311 | downloaded_file = {} 312 | downloaded_file.request_frame = pinfo.number 313 | downloaded_file.data_frames = {} 314 | downloaded_file.count = 0 315 | else 316 | local frames = cached_downloads[pinfo.number].frames 317 | tree:add(download_request_data, frames[#frames].num) 318 | :set_generated(true) 319 | end 320 | end 321 | vivofit_table:add(0x138a, vivofit_download_request) 322 | -- 323 | -- 324 | -- Begin Download Request Reply 0x138a-R 325 | -- 326 | vivofit_download_request_reply = Proto("vivofit.download_request_reply", "Vivofit Download Request Reply") 327 | download_request_reply_status = ProtoField.uint8("vivofit.download_request_reply.status", "Status", base.HEX) 328 | download_request_reply_length = ProtoField.uint32("vivofit.download_request_reply.length", "Length") 329 | vivofit_download_request_reply.fields = { 330 | download_request_reply_status, 331 | download_request_reply_length, 332 | } 333 | function vivofit_download_request_reply.dissector(buffer, pinfo, root) 334 | pinfo.cols.info = "Download Request Reply" 335 | local tree = root:add(vivofit_download_request_reply, buffer()) 336 | tree:add_le(download_request_reply_status, buffer(0, 1)) 337 | tree:add_le(download_request_reply_length, buffer(1, 4)) 338 | 339 | if not pinfo.visited then 340 | assert(downloaded_file.request_frame ~= nil) 341 | downloaded_file.len = buffer(1, 4):le_uint() 342 | end 343 | end 344 | vivofit_ack_table:add(0x138a, vivofit_download_request_reply) 345 | -- 346 | -- 347 | -- Begin File Data 0x138c 348 | -- 349 | vivofit_file_data = Proto("vivofit.file_data", "Vivofit File Data") 350 | file_data_flags = ProtoField.uint8("vivofit.file_data.flags", "File Data Flags", base.HEX) 351 | file_data_crc = ProtoField.uint16("vivofit.file_data.crc", "File Data CRC", base.HEX) 352 | file_data_offset = ProtoField.uint16("vivofit.file_data.offset", "File Data Offset") 353 | file_data_data = ProtoField.bytes("vivofit.file_data.data", "File Data") 354 | file_data_full = ProtoField.bytes("vivofit.file_data.data_full", "Full File Data") 355 | file_data_fragment = ProtoField.framenum("vivofit.file_data.fragment", "File Data Fragment") 356 | vivofit_file_data.fields = { 357 | file_data_flags, 358 | file_data_crc, 359 | file_data_offset, 360 | file_data_data, 361 | file_data_full, 362 | file_data_fragment, 363 | } 364 | -- if you want to decode these further, see here: https://pub.ks-and-ks.ne.jp/cycling/edge500_fit.shtml 365 | function vivofit_file_data.dissector(buffer, pinfo, root) 366 | pinfo.cols.info = "File Data" 367 | local tree = root:add(vivofit_file_data, buffer()) 368 | tree:add_le(file_data_flags, buffer(0, 1)) 369 | tree:add_le(file_data_crc, buffer(1, 2)) 370 | tree:add_le(file_data_offset, buffer(3, 4)) 371 | tree:add_le(file_data_data, buffer(7)) 372 | 373 | if not pinfo.visited then 374 | dframes = downloaded_file.data_frames 375 | assert(dframes ~= nil) 376 | dframes[#dframes + 1] = { 377 | buf = buffer(7):bytes(), 378 | num = pinfo.number, 379 | len = buffer(7):bytes():len() 380 | } 381 | assert(downloaded_file.count ~= nil) 382 | downloaded_file.count = downloaded_file.count + buffer:bytes():len() 383 | 384 | assert(downloaded_file.len ~= nil) 385 | if downloaded_file.count >= downloaded_file.len then 386 | local final_data = {} 387 | 388 | local abuf = ByteArray.new() 389 | final_data.buf = abuf 390 | 391 | local frames = {} 392 | final_data.frames = frames 393 | 394 | cached_downloads[downloaded_file.request_frame] = final_data 395 | for i = 1,#dframes,1 do 396 | frames[i] = { 397 | num = dframes[i].num, 398 | len = dframes[i].len 399 | } 400 | abuf:append(dframes[i].buf) 401 | cached_downloads[dframes[i].num] = final_data 402 | end 403 | downloaded_file = nil 404 | end 405 | else 406 | local frames = cached_downloads[pinfo.number].frames 407 | if frames[#frames].num == pinfo.number then 408 | local data = cached_downloads[pinfo.number].buf:tvb("File Data") 409 | local defragged = root:add(file_data_full, data()) 410 | :set_generated(true) 411 | 412 | local count = 0 413 | for i = 1,#frames,1 do 414 | defragged:add( 415 | file_data_fragment, 416 | data(count, frames[i].len), 417 | frames[i].num 418 | ):set_generated(true) 419 | count = count + frames[i].len 420 | end 421 | else 422 | root:add(file_data_fragment, frames[#frames].num) 423 | :set_generated(true) 424 | end 425 | end 426 | end 427 | vivofit_table:add(0x138c, vivofit_file_data) 428 | -- 429 | -- 430 | -- Begin File Data Reply 0x138c-R 431 | -- 432 | vivofit_file_data_reply = Proto("vivofit.file_data_reply", "Vivofit File Data Reply") 433 | file_data_reply_status = ProtoField.uint8("vivofit.file_data_reply.status", "File Data Status", base.HEX) 434 | file_data_reply_offset = ProtoField.uint16("vivofit.file_data_reply.offset", "File Data Offset") 435 | vivofit_file_data_reply.fields = { 436 | file_data_reply_status, 437 | file_data_reply_offset, 438 | } 439 | function vivofit_file_data_reply.dissector(buffer, pinfo, root) 440 | pinfo.cols.info = "File Data Reply" 441 | local tree = root:add(vivofit_file_data_reply, buffer()) 442 | tree:add_le(file_data_reply_status, buffer(0, 1)) 443 | tree:add_le(file_data_reply_offset, buffer(1, 4)) 444 | -- print("vivofit_system_event: " .. tostring(pinfo.number)) 445 | end 446 | vivofit_ack_table:add(0x138c, vivofit_file_data_reply) 447 | --------------------------------------------------------------------------------