From 81d4e308a36cd06f5e7bac30d2af10145c8f193a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sun, 4 Oct 2020 15:12:44 +0100 Subject: Depend on freestyle-hid for FreeStyle support and remove tools. Instead of maintaining the reversing tools for Abbott FreeStyle devices in this repository, they are now part of their own project (https://github.com/glucometers-tech/freestyle-hid), making it easier to split the dependencies requirements. The basic I/O of the FreeStyle session is also implemented in that library. --- LICENSES/Apache-2.0.txt | 208 ---------------- README.md | 11 +- glucometerutils/support/freestyle.py | 275 ++------------------- glucometerutils/support/tests/test_freestyle.py | 24 -- reversing_tools/__init__.py | 3 - reversing_tools/abbott/__init__.py | 3 - .../abbott/encrypted_setup_extractor.py | 179 -------------- reversing_tools/abbott/extract_freestyle.py | 245 ------------------ reversing_tools/abbott/freestyle_hid_console.py | 69 ------ reversing_tools/abbott/known-commands.txt | 48 ---- reversing_tools/abbott/known-commands.txt.license | 3 - setup.py | 8 +- 12 files changed, 25 insertions(+), 1051 deletions(-) delete mode 100644 LICENSES/Apache-2.0.txt delete mode 100644 glucometerutils/support/tests/test_freestyle.py delete mode 100644 reversing_tools/__init__.py delete mode 100644 reversing_tools/abbott/__init__.py delete mode 100644 reversing_tools/abbott/encrypted_setup_extractor.py delete mode 100755 reversing_tools/abbott/extract_freestyle.py delete mode 100755 reversing_tools/abbott/freestyle_hid_console.py delete mode 100644 reversing_tools/abbott/known-commands.txt delete mode 100644 reversing_tools/abbott/known-commands.txt.license diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt deleted file mode 100644 index 527a83a..0000000 --- a/LICENSES/Apache-2.0.txt +++ /dev/null @@ -1,208 +0,0 @@ -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, -AND DISTRIBUTION - - 1. Definitions. - - - -"License" shall mean the terms and conditions for use, reproduction, and distribution -as defined by Sections 1 through 9 of this document. - - - -"Licensor" shall mean the copyright owner or entity authorized by the copyright -owner that is granting the License. - - - -"Legal Entity" shall mean the union of the acting entity and all other entities -that control, are controlled by, or are under common control with that entity. -For the purposes of this definition, "control" means (i) the power, direct -or indirect, to cause the direction or management of such entity, whether -by contract or otherwise, or (ii) ownership of fifty percent (50%) or more -of the outstanding shares, or (iii) beneficial ownership of such entity. - - - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions -granted by this License. - - - -"Source" form shall mean the preferred form for making modifications, including -but not limited to software source code, documentation source, and configuration -files. - - - -"Object" form shall mean any form resulting from mechanical transformation -or translation of a Source form, including but not limited to compiled object -code, generated documentation, and conversions to other media types. - - - -"Work" shall mean the work of authorship, whether in Source or Object form, -made available under the License, as indicated by a copyright notice that -is included in or attached to the work (an example is provided in the Appendix -below). - - - -"Derivative Works" shall mean any work, whether in Source or Object form, -that is based on (or derived from) the Work and for which the editorial revisions, -annotations, elaborations, or other modifications represent, as a whole, an -original work of authorship. For the purposes of this License, Derivative -Works shall not include works that remain separable from, or merely link (or -bind by name) to the interfaces of, the Work and Derivative Works thereof. - - - -"Contribution" shall mean any work of authorship, including the original version -of the Work and any modifications or additions to that Work or Derivative -Works thereof, that is intentionally submitted to Licensor for inclusion in -the Work by the copyright owner or by an individual or Legal Entity authorized -to submit on behalf of the copyright owner. For the purposes of this definition, -"submitted" means any form of electronic, verbal, or written communication -sent to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, and -issue tracking systems that are managed by, or on behalf of, the Licensor -for the purpose of discussing and improving the Work, but excluding communication -that is conspicuously marked or otherwise designated in writing by the copyright -owner as "Not a Contribution." - - - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf -of whom a Contribution has been received by Licensor and subsequently incorporated -within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this -License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, -no-charge, royalty-free, irrevocable copyright license to reproduce, prepare -Derivative Works of, publicly display, publicly perform, sublicense, and distribute -the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, -each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, -no-charge, royalty-free, irrevocable (except as stated in this section) patent -license to make, have made, use, offer to sell, sell, import, and otherwise -transfer the Work, where such license applies only to those patent claims -licensable by such Contributor that are necessarily infringed by their Contribution(s) -alone or by combination of their Contribution(s) with the Work to which such -Contribution(s) was submitted. If You institute patent litigation against -any entity (including a cross-claim or counterclaim in a lawsuit) alleging -that the Work or a Contribution incorporated within the Work constitutes direct -or contributory patent infringement, then any patent licenses granted to You -under this License for that Work shall terminate as of the date such litigation -is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or -Derivative Works thereof in any medium, with or without modifications, and -in Source or Object form, provided that You meet the following conditions: - -(a) You must give any other recipients of the Work or Derivative Works a copy -of this License; and - -(b) You must cause any modified files to carry prominent notices stating that -You changed the files; and - -(c) You must retain, in the Source form of any Derivative Works that You distribute, -all copyright, patent, trademark, and attribution notices from the Source -form of the Work, excluding those notices that do not pertain to any part -of the Derivative Works; and - -(d) If the Work includes a "NOTICE" text file as part of its distribution, -then any Derivative Works that You distribute must include a readable copy -of the attribution notices contained within such NOTICE file, excluding those -notices that do not pertain to any part of the Derivative Works, in at least -one of the following places: within a NOTICE text file distributed as part -of the Derivative Works; within the Source form or documentation, if provided -along with the Derivative Works; or, within a display generated by the Derivative -Works, if and wherever such third-party notices normally appear. The contents -of the NOTICE file are for informational purposes only and do not modify the -License. You may add Your own attribution notices within Derivative Works -that You distribute, alongside or as an addendum to the NOTICE text from the -Work, provided that such additional attribution notices cannot be construed -as modifying the License. - -You may add Your own copyright statement to Your modifications and may provide -additional or different license terms and conditions for use, reproduction, -or distribution of Your modifications, or for any such Derivative Works as -a whole, provided Your use, reproduction, and distribution of the Work otherwise -complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any -Contribution intentionally submitted for inclusion in the Work by You to the -Licensor shall be under the terms and conditions of this License, without -any additional terms or conditions. Notwithstanding the above, nothing herein -shall supersede or modify the terms of any separate license agreement you -may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, -trademarks, service marks, or product names of the Licensor, except as required -for reasonable and customary use in describing the origin of the Work and -reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to -in writing, Licensor provides the Work (and each Contributor provides its -Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, either express or implied, including, without limitation, any warranties -or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR -A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness -of using or redistributing the Work and assume any risks associated with Your -exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether -in tort (including negligence), contract, or otherwise, unless required by -applicable law (such as deliberate and grossly negligent acts) or agreed to -in writing, shall any Contributor be liable to You for damages, including -any direct, indirect, special, incidental, or consequential damages of any -character arising as a result of this License or out of the use or inability -to use the Work (including but not limited to damages for loss of goodwill, -work stoppage, computer failure or malfunction, or any and all other commercial -damages or losses), even if such Contributor has been advised of the possibility -of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work -or Derivative Works thereof, You may choose to offer, and charge a fee for, -acceptance of support, warranty, indemnity, or other liability obligations -and/or rights consistent with this License. However, in accepting such obligations, -You may act only on Your own behalf and on Your sole responsibility, not on -behalf of any other Contributor, and only if You agree to indemnify, defend, -and hold each Contributor harmless for any liability incurred by, or claims -asserted against, such Contributor by reason of your accepting any such warranty -or additional liability. END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate -notice, with the fields enclosed by brackets "[]" replaced with your own identifying -information. (Don't include the brackets!) The text should be enclosed in -the appropriate comment syntax for the file format. We also recommend that -a file or class name and description of purpose be included on the same "printed -page" as the copyright notice for easier identification within third-party -archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); - -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software - -distributed under the License is distributed on an "AS IS" BASIS, - -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and - -limitations under the License. diff --git a/README.md b/README.md index 560e454..92e4ad0 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,12 @@ supported. | LifeScan | OneTouch Verio (USB) | `otverio2015` | [construct] [python-scsi] | | LifeScan | OneTouch Select Plus | `otverio2015` | [construct] [python-scsi] | | LifeScan | OneTouch Select Plus Flex¹ | `otverio2015` | [construct] [python-scsi] | -| Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [construct] [hidapi]‡ | -| Abbott | FreeStyle Libre | `fslibre` | [construct] [hidapi]‡ | +| Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [freestyle_hid] [hidapi]‡ | +| Abbott | FreeStyle Libre | `fslibre` | [freestyle_hid] [hidapi]‡ | | Abbott | FreeStyle Optium | `fsoptium` | [pyserial] | -| Abbott | FreeStyle Precision Neo | `fsprecisionneo` | [construct] [hidapi]‡ | -| Abbott | FreeStyle Optium Neo | `fsprecisionneo` | [construct] [hidapi]‡ | -| Abbott | FreeStyle Optium Neo H | `fsprecisionneo` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Precision Neo | `fsprecisionneo` | [freestyle_hid] [hidapi]‡ | +| Abbott | FreeStyle Optium Neo | `fsprecisionneo` | [freestyle_hid] [hidapi]‡ | +| Abbott | FreeStyle Optium Neo H | `fsprecisionneo` | [freestyle_hid] [hidapi]‡ | | Roche | Accu-Chek Mobile | `accuchek_reports` | | | SD Biosensor | SD CodeFree | `sdcodefree` | [construct] [pyserial] | | TaiDoc | TD-4277 | `td4277` | [construct] [pyserial]² [hidapi] | @@ -86,6 +86,7 @@ please provide a reference, possibly by writing a specification and contribute it to https://protocols.glucometers.tech/ . [construct]: https://construct.readthedocs.io/en/latest/ +[freestyle-hid]: https://pypi.org/project/freestyle-hid/ [pyserial]: https://pythonhosted.org/pyserial/ [python-scsi]: https://pypi.org/project/PYSCSI/ [hidapi]: https://pypi.python.org/pypi/hidapi diff --git a/glucometerutils/support/freestyle.py b/glucometerutils/support/freestyle.py index 13e48eb..28aec6a 100644 --- a/glucometerutils/support/freestyle.py +++ b/glucometerutils/support/freestyle.py @@ -9,110 +9,13 @@ https://protocols.glucometers.tech/abbott/shared-hid-protocol """ -import csv import datetime -import logging -import re -from typing import AnyStr, Callable, Iterator, List, Optional, Tuple +import pathlib +from typing import Optional -import construct +import freestyle_hid from glucometerutils import driver, exceptions -from glucometerutils.support import hiddevice - -_INIT_COMMAND = 0x01 -_INIT_RESPONSE = 0x71 - -_KEEPALIVE_RESPONSE = 0x22 -_UNKNOWN_MESSAGE_RESPONSE = 0x30 - -_ENCRYPTION_SETUP_COMMAND = 0x14 -_ENCRYPTION_SETUP_RESPONSE = 0x33 - -_ALWAYS_UNENCRYPTED_MESSAGES = ( - _INIT_COMMAND, - 0x04, - 0x05, - 0x06, - 0x0C, - 0x0D, - _ENCRYPTION_SETUP_COMMAND, - 0x15, - _ENCRYPTION_SETUP_RESPONSE, - 0x34, - 0x35, - _INIT_RESPONSE, - _KEEPALIVE_RESPONSE, -) - - -def _create_matcher( - message_type: int, content: Optional[bytes] -) -> Callable[[Tuple[int, bytes]], bool]: - def _matcher(message: Tuple[int, bytes]) -> bool: - return message[0] == message_type and (content is None or content == message[1]) - - return _matcher - - -_is_init_reply = _create_matcher(_INIT_RESPONSE, b"\x01") -_is_keepalive_response = _create_matcher(_KEEPALIVE_RESPONSE, None) -_is_unknown_message_error = _create_matcher(_UNKNOWN_MESSAGE_RESPONSE, b"\x85") -_is_encryption_missing_error = _create_matcher(_ENCRYPTION_SETUP_RESPONSE, b"\x15") -_is_encryption_setup_error = _create_matcher(_ENCRYPTION_SETUP_RESPONSE, b"\x14") - -_FREESTYLE_MESSAGE = construct.Struct( - hid_report=construct.Const(0, construct.Byte), - message_type=construct.Byte, - command=construct.Padded( - 63, # command can only be up to 62 bytes, but one is used for length. - construct.Prefixed(construct.Byte, construct.GreedyBytes), - ), -) - -_FREESTYLE_ENCRYPTED_MESSAGE = construct.Struct( - hid_report=construct.Const(0, construct.Byte), - message_type=construct.Byte, - command=construct.Padded( - 63, # command can only be up to 62 bytes, but one is used for length. - construct.GreedyBytes, - ), -) - -_TEXT_COMPLETION_RE = re.compile(b"CMD (?:OK|Fail!)") -_TEXT_REPLY_FORMAT = re.compile( - b"^(?P.*)CKSM:(?P[0-9A-F]{8})\r\n" - b"CMD (?POK|Fail!)\r\n$", - re.DOTALL, -) - -_MULTIRECORDS_FORMAT = re.compile( - "^(?P.+\r\n)(?P[0-9]+),(?P[0-9A-F]{8})\r\n$", re.DOTALL -) - - -def _verify_checksum(message: AnyStr, expected_checksum_hex: AnyStr) -> None: - """Calculate the simple checksum of the message and compare with expected. - - Args: - message: (str) message to calculate the checksum of. - expected_checksum_hex: hexadecimal string representing the checksum - expected to match the message. - - Raises: - InvalidChecksum: if the message checksum calculated does not match the one - received. - """ - expected_checksum = int(expected_checksum_hex, 16) - if isinstance(message, bytes): - all_bytes = (c for c in message) - else: - all_bytes = (ord(c) for c in message) - - calculated_checksum = sum(all_bytes) - - if expected_checksum != calculated_checksum: - raise exceptions.InvalidChecksum(expected_checksum, calculated_checksum) def convert_ketone_unit(raw_value: float) -> float: @@ -129,161 +32,6 @@ def convert_ketone_unit(raw_value: float) -> float: ABBOTT_VENDOR_ID = 0x1A61 -class FreeStyleHidSession: - def __init__( - self, - product_id: int, - device_path: Optional[str], - text_message_type: int, - text_reply_message_type: int, - ) -> None: - - self._hid_session = hiddevice.HidSession( - (ABBOTT_VENDOR_ID, product_id), device_path - ) - self._text_message_type = text_message_type - self._text_reply_message_type = text_reply_message_type - - def connect(self): - """Open connection to the device, starting the knocking sequence.""" - self.send_command(_INIT_COMMAND, b"") - response = self.read_response() - if not _is_init_reply(response): - raise exceptions.ConnectionFailed( - f"Connection error: unexpected message %{response[0]:02x}:{response[1].hex()}" - ) - - def send_command(self, message_type: int, command: bytes, encrypted: bool = False): - """Send a raw command to the device. - - Args: - message_type: The first byte sent with the report to the device. - command: The command to send out the device. - """ - if encrypted: - assert message_type not in _ALWAYS_UNENCRYPTED_MESSAGES - meta_construct = _FREESTYLE_ENCRYPTED_MESSAGE - else: - meta_construct = _FREESTYLE_MESSAGE - - usb_packet = meta_construct.build( - {"message_type": message_type, "command": command} - ) - - logging.debug("Sending packet: %r", usb_packet) - self._hid_session.write(usb_packet) - - def read_response(self, encrypted: bool = False) -> Tuple[int, bytes]: - """Read the response from the device and extracts it.""" - usb_packet = self._hid_session.read() - - logging.debug("Read packet: %r", usb_packet) - - assert usb_packet - message_type = usb_packet[0] - - if not encrypted or message_type in _ALWAYS_UNENCRYPTED_MESSAGES: - message_length = usb_packet[1] - message_end_idx = 2 + message_length - message_content = usb_packet[2:message_end_idx] - else: - message_content = usb_packet[1:] - - # hidapi module returns a list of bytes rather than a bytes object. - message = (message_type, bytes(message_content)) - - # There appears to be a stray number of 22 01 xx messages being returned - # by some devices after commands are sent. These do not appear to have - # meaning, so ignore them and proceed to the next. These are always sent - # unencrypted, so we need to inspect them before we decide what the - # message content is. - if _is_keepalive_response(message): - return self.read_response(encrypted=encrypted) - - if _is_unknown_message_error(message): - raise exceptions.CommandError("Invalid command") - - if _is_encryption_missing_error(message): - raise exceptions.CommandError("Device encryption not initialized.") - - if _is_encryption_setup_error(message): - raise exceptions.CommandError("Device encryption initialization failed.") - - return message - - def send_text_command(self, command: bytes) -> str: - """Send a command to the device that expects a text reply.""" - self.send_command(self._text_message_type, command) - - # Reply can stretch multiple buffers - full_content = b"" - while True: - message_type, content = self.read_response() - - logging.debug( - "Received message: type %02x content %s", message_type, content.hex() - ) - - if message_type != self._text_reply_message_type: - raise exceptions.InvalidResponse( - f"Message type {message_type:02x}: content does not match expectations: {content!r}" - ) - - full_content += content - - if _TEXT_COMPLETION_RE.search(full_content): - break - - match = _TEXT_REPLY_FORMAT.search(full_content) - if not match: - raise exceptions.InvalidResponse(repr(full_content)) - - message = match.group("message") - _verify_checksum(message, match.group("checksum")) - - if match.group("status") != b"OK": - raise exceptions.InvalidResponse(repr(message) or "Command failed") - - # If there is anything in the response that is not ASCII-safe, this is - # probably in the patient name. The Windows utility does not seem to - # validate those, so just replace anything non-ASCII with the correct - # unknown codepoint. - return message.decode("ascii", "replace") - - def query_multirecord(self, command: bytes) -> Iterator[List[str]]: - """Queries for, and returns, "multirecords" results. - - Multirecords are used for querying events, readings, history and similar - other data out of a FreeStyle device. These are comma-separated values, - variable-length. - - The validation includes the general HID framing parsing, as well as - validation of the record count, and of the embedded records checksum. - - Args: - command: The text command to send to the device for the query. - - Returns: - A CSV reader object that returns a record for each line in the - reply buffer. - """ - message = self.send_text_command(command) - logging.debug("Received multirecord message:\n%s", message) - if message == "Log Empty\r\n": - return iter(()) - - match = _MULTIRECORDS_FORMAT.search(message) - if not match: - raise exceptions.InvalidResponse(message) - - records_str = match.group("message") - _verify_checksum(records_str, match.group("checksum")) - - logging.debug("Received multi-record string: %s", records_str) - - return csv.reader(records_str.split("\r\n")) - - class FreeStyleHidDevice(driver.GlucometerDevice): """Base class implementing the FreeStyle HID common protocol. @@ -304,13 +52,22 @@ class FreeStyleHidDevice(driver.GlucometerDevice): text_reply_cmd: int = 0x60, ) -> None: super().__init__(device_path) - self._session = FreeStyleHidSession( - product_id, device_path, text_cmd, text_reply_cmd - ) + try: + self._session = freestyle_hid.Session( + product_id, + pathlib.Path(device_path) if device_path else None, + text_cmd, + text_reply_cmd, + ) + except Exception as e: + raise exceptions.ConnectionFailed(str(e)) def connect(self) -> None: """Open connection to the device, starting the knocking sequence.""" - self._session.connect() + try: + self._session.connect() + except Exception as e: + raise exceptions.ConnectionFailed(str(e)) def disconnect(self) -> None: """Disconnect the device, nothing to be done.""" diff --git a/glucometerutils/support/tests/test_freestyle.py b/glucometerutils/support/tests/test_freestyle.py deleted file mode 100644 index fd3f403..0000000 --- a/glucometerutils/support/tests/test_freestyle.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# -# SPDX-FileCopyrightText: © 2019 The glucometerutils Authors -# SPDX-License-Identifier: MIT -"""Tests for the common FreeStyle functions..""" - -# pylint: disable=protected-access,missing-docstring - -from absl.testing import absltest - -from glucometerutils.support import freestyle - - -class TestFreeStyle(absltest.TestCase): - def test_outgoing_command(self): - """Test the generation of a new outgoing message.""" - - self.assertEqual( - b"\0\x17\7command\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" - b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", - freestyle._FREESTYLE_MESSAGE.build( - {"message_type": 23, "command": b"command"} - ), - ) diff --git a/reversing_tools/__init__.py b/reversing_tools/__init__.py deleted file mode 100644 index 4b386c3..0000000 --- a/reversing_tools/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-FileCopyrightText: 2013 The glucometerutils Authors -# -# SPDX-License-Identifier: Unlicense diff --git a/reversing_tools/abbott/__init__.py b/reversing_tools/abbott/__init__.py deleted file mode 100644 index 4b386c3..0000000 --- a/reversing_tools/abbott/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-FileCopyrightText: 2013 The glucometerutils Authors -# -# SPDX-License-Identifier: Unlicense diff --git a/reversing_tools/abbott/encrypted_setup_extractor.py b/reversing_tools/abbott/encrypted_setup_extractor.py deleted file mode 100644 index cc57f0f..0000000 --- a/reversing_tools/abbott/encrypted_setup_extractor.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env python3 -# -# SPDX-FileCopyrightText: © 2019 The usbmon-tools Authors -# SPDX-FileCopyrightText: © 2020 The glucometerutils Authors -# -# SPDX-License-Identifier: Apache-2.0 - -import argparse -import logging -import sys - -import construct -import usbmon -import usbmon.pcapng - -_SERIAL_NUMBER_RESPONSE_TYPE = 0x06 -_ENCRYPTION_SETUP_REQ_TYPE = 0x14 -_ENCRYPTION_SETUP_RESP_TYPE = 0x33 - - -_START_AUTHORIZE_CMD = 0x11 -_CHALLENGE_CMD = 0x16 -_CHALLENGE_RESPONSE_CMD = 0x17 - - -_ABBOTT_VENDOR_ID = 0x1A61 -_LIBRE2_PRODUCT_ID = 0x3950 - -_SERIAL_NO = construct.Struct( - message_type=construct.Const(_SERIAL_NUMBER_RESPONSE_TYPE, construct.Byte), - length=construct.Const(14, construct.Byte), - serial_number=construct.PaddedString(13, "ascii"), - termination=construct.Const(0, construct.Byte), -) - -_CHALLENGE = construct.Struct( - message_type=construct.Const(_ENCRYPTION_SETUP_RESP_TYPE, construct.Byte), - length=construct.Const(16, construct.Byte), - subcmd=construct.Const(_CHALLENGE_CMD, construct.Byte), - challenge=construct.Bytes(8), - iv=construct.Bytes(7), -) - -_CHALLENGE_RESPONSE = construct.Struct( - message_type=construct.Const(_ENCRYPTION_SETUP_REQ_TYPE, construct.Byte), - length=construct.Const(26, construct.Byte), - subcmd=construct.Const(_CHALLENGE_RESPONSE_CMD, construct.Byte), - challenge_response_encrypted=construct.Bytes(16), - const=construct.Const(1, construct.Byte), - mac=construct.Bytes(8), -) - - -def main(): - if sys.version_info < (3, 7): - raise Exception("Unsupported Python version, please use at least Python 3.7.") - - parser = argparse.ArgumentParser() - - parser.add_argument( - "--device_address", - action="store", - type=str, - help=( - "Device address (busnum.devnum) of the device to extract capture" - " of. If none provided, device descriptors will be relied on." - ), - ) - - parser.add_argument( - "--vlog", - action="store", - required=False, - type=int, - help=( - "Python logging level. See the levels at" - " https://docs.python.org/3/library/logging.html#logging-levels" - ), - ) - - parser.add_argument( - "pcap_files", - action="store", - type=argparse.FileType(mode="rb"), - help="Path to the pcapng file with the USB capture.", - nargs="+", - ) - - args = parser.parse_args() - - logging.basicConfig(level=args.vlog) - - for pcap_file in args.pcap_files: - session = usbmon.pcapng.parse_stream(pcap_file, retag_urbs=False) - - if not args.device_address: - for descriptor in session.device_descriptors.values(): - if ( - descriptor.vendor_id == _ABBOTT_VENDOR_ID - and descriptor.product_id == _LIBRE2_PRODUCT_ID - ): - if ( - args.device_address - and args.device_address != descriptor.address - ): - raise Exception( - "Multiple Libre2 devices present in capture, please" - " provide a --device_address flag." - ) - device_address = descriptor.address - else: - device_address = descriptor.address - - descriptor = session.device_descriptors.get(device_address, None) - if descriptor: - assert descriptor.vendor_id == _ABBOTT_VENDOR_ID - assert descriptor.product_id == _LIBRE2_PRODUCT_ID - - serial_number = "UNKNOWN" - challenge = "UNKNOWN" - iv = "UNKNOWN" - encrypted_challenge = "UNKNOWN" - mac = "UNKNOWN" - - for first, second in session.in_pairs(): - # Ignore stray callbacks/errors. - if not first.type == usbmon.constants.PacketType.SUBMISSION: - continue - - if not first.address.startswith(f"{device_address}."): - # No need to check second, they will be linked. - continue - - if first.xfer_type == usbmon.constants.XferType.INTERRUPT: - pass - elif ( - first.xfer_type == usbmon.constants.XferType.CONTROL - and not first.setup_packet - or first.setup_packet.type == usbmon.setup.Type.CLASS - ): - pass - else: - continue - - if first.direction == usbmon.constants.Direction.OUT: - packet = first - else: - packet = second - - if not packet.payload: - continue - - assert len(packet.payload) >= 2 - - message_type = packet.payload[0] - - if message_type == _SERIAL_NUMBER_RESPONSE_TYPE: - obj = _SERIAL_NO.parse(packet.payload) - serial_number = obj.serial_number - elif ( - message_type == _ENCRYPTION_SETUP_RESP_TYPE - and packet.payload[2] == _CHALLENGE_CMD - ): - obj = _CHALLENGE.parse(packet.payload) - challenge = obj.challenge.hex() - iv = obj.iv.hex() - elif ( - message_type == _ENCRYPTION_SETUP_REQ_TYPE - and packet.payload[2] == _CHALLENGE_RESPONSE_CMD - ): - obj = _CHALLENGE_RESPONSE.parse(packet.payload) - encrypted_challenge = obj.challenge_response_encrypted.hex() - mac = obj.mac.hex() - - print(f"{serial_number},{challenge},{iv},{encrypted_challenge},{mac}") - - -if __name__ == "__main__": - main() diff --git a/reversing_tools/abbott/extract_freestyle.py b/reversing_tools/abbott/extract_freestyle.py deleted file mode 100755 index 0c0888a..0000000 --- a/reversing_tools/abbott/extract_freestyle.py +++ /dev/null @@ -1,245 +0,0 @@ -#!/usr/bin/env python3 -# -# SPDX-FileCopyrightText: © 2019 The usbmon-tools Authors -# SPDX-FileCopyrightText: © 2020 The glucometerutils Authors -# -# SPDX-License-Identifier: Apache-2.0 - -import argparse -import logging -import sys -import textwrap - -import construct -import usbmon -import usbmon.chatter -import usbmon.pcapng - -_KEEPALIVE_TYPE = 0x22 - -_UNENCRYPTED_TYPES = ( - 0x01, - 0x04, - 0x05, - 0x06, - 0x0C, - 0x0D, - 0x14, - 0x15, - 0x33, - 0x34, - 0x35, - 0x71, - _KEEPALIVE_TYPE, -) - -_ENCRYPTION_SETUP_TYPES = (0x14, 0x33) - -_START_AUTHORIZE_CMD = 0x11 -_CHALLENGE_CMD = 0x16 -_CHALLENGE_RESPONSE_CMD = 0x17 -_CHALLENGE_ACCEPTED_CMD = 0x18 - -_ABBOTT_VENDOR_ID = 0x1A61 -_LIBRE2_PRODUCT_ID = 0x3950 - -_ENCRYPTED_MESSAGE = construct.Struct( - message_type=construct.Byte, - encrypted_message=construct.Bytes(64 - 1 - 4 - 4), - sequence_number=construct.Int32ul, - mac=construct.Int32ul, -) - - -def main(): - if sys.version_info < (3, 7): - raise Exception("Unsupported Python version, please use at least Python 3.7.") - - parser = argparse.ArgumentParser() - - parser.add_argument( - "--device_address", - action="store", - type=str, - help=( - "Device address (busnum.devnum) of the device to extract capture" - " of. If none provided, device descriptors will be relied on." - ), - ) - - parser.add_argument( - "--encrypted_protocol", - action="store_true", - help=( - "Whether to expect encrypted protocol in the capture." - " Ignored if the device descriptors are present in the capture." - ), - ) - - parser.add_argument( - "--verbose-encryption-setup", - action="store_true", - help=( - "Whether to parse encryption setup commands and printing their component" - " together with the raw messsage." - ), - ) - - parser.add_argument( - "--vlog", - action="store", - required=False, - type=int, - help=( - "Python logging level. See the levels at" - " https://docs.python.org/3/library/logging.html#logging-levels" - ), - ) - - parser.add_argument( - "--print_keepalive", - action="store_true", - help=( - "Whether to print the keepalive messages sent by the device. " - "Keepalive messages are usually safely ignored." - ), - ) - - parser.add_argument( - "pcap_file", - action="store", - type=argparse.FileType(mode="rb"), - help="Path to the pcapng file with the USB capture.", - ) - - args = parser.parse_args() - - logging.basicConfig(level=args.vlog) - - session = usbmon.pcapng.parse_stream(args.pcap_file, retag_urbs=False) - - if not args.device_address: - for descriptor in session.device_descriptors.values(): - if descriptor.vendor_id == _ABBOTT_VENDOR_ID: - if args.device_address and args.device_address != descriptor.address: - raise Exception( - "Multiple Abbott device present in capture, please" - " provide a --device_address flag." - ) - args.device_address = descriptor.address - - descriptor = session.device_descriptors.get(args.device_address, None) - if not descriptor: - logging.warning( - "Unable to find device %s in the capture's descriptors." - " Assuming non-encrypted protocol.", - args.device_address, - ) - else: - assert descriptor.vendor_id == _ABBOTT_VENDOR_ID - - if descriptor and descriptor.product_id == _LIBRE2_PRODUCT_ID: - args.encrypted_protocol = True - - for first, second in session.in_pairs(): - # Ignore stray callbacks/errors. - if not first.type == usbmon.constants.PacketType.SUBMISSION: - continue - - if not first.address.startswith(f"{args.device_address}."): - # No need to check second, they will be linked. - continue - - if first.xfer_type == usbmon.constants.XferType.INTERRUPT: - pass - elif ( - first.xfer_type == usbmon.constants.XferType.CONTROL - and not first.setup_packet - or first.setup_packet.type == usbmon.setup.Type.CLASS - ): - pass - else: - continue - - if first.direction == usbmon.constants.Direction.OUT: - packet = first - else: - packet = second - - if not packet.payload: - continue - - assert len(packet.payload) >= 2 - - message_type = packet.payload[0] - - if message_type == _KEEPALIVE_TYPE and not args.print_keepalive: - continue - - message_metadata = [] - - if args.encrypted_protocol and message_type not in _UNENCRYPTED_TYPES: - # With encrypted communication, the length of the message is also encrypted, - # and all the packets use the full 64 bytes. So instead, we extract what - # metadata we can. - parsed = _ENCRYPTED_MESSAGE.parse(packet.payload) - message_metadata.extend( - [f"SEQUENCE_NUMBER={parsed.sequence_number}", f"MAC={parsed.mac:04x}"] - ) - - message_type = f"x{message_type:02x}" - message = parsed.encrypted_message - elif args.verbose_encryption_setup and message_type in _ENCRYPTION_SETUP_TYPES: - message_length = packet.payload[1] - message_end_idx = 2 + message_length - message = packet.payload[2:message_end_idx] - - if message[0] == _START_AUTHORIZE_CMD: - message_metadata.append("START_AUTHORIZE") - elif message[0] == _CHALLENGE_CMD: - message_metadata.append("CHALLENGE") - challenge = message[1:9] - iv = message[9:16] - message_metadata.append(f"CHALLENGE={challenge.hex()}") - message_metadata.append(f"IV={iv.hex()}") - elif message[0] == _CHALLENGE_RESPONSE_CMD: - message_metadata.append("CHALLENGE_RESPONSE") - encrypted_challenge = message[1:17] - challenge_mac = message[18:26] - message_metadata.append( - f"ENCRYPTED_CHALLENGE={encrypted_challenge.hex()}" - ) - message_metadata.append(f"MAC={challenge_mac.hex()}") - elif message[0] == _CHALLENGE_ACCEPTED_CMD: - message_metadata.append("CHALLENGE_ACCEPTED") - - message_metadata.append(f"RAW_LENGTH={message_length}") - message_type = f" {message_type:02x}" - else: - message_length = packet.payload[1] - message_metadata.append(f"LENGTH={message_length}") - message_end_idx = 2 + message_length - message_type = f" {message_type:02x}" - message = packet.payload[2:message_end_idx] - - if message_metadata: - metadata_string = "\n".join( - textwrap.wrap( - " ".join(message_metadata), width=80, break_long_words=False - ) - ) - print(metadata_string) - - print( - usbmon.chatter.dump_bytes( - packet.direction, - message, - prefix=f"[{message_type}]", - print_empty=True, - ), - "\n", - ) - - -if __name__ == "__main__": - main() diff --git a/reversing_tools/abbott/freestyle_hid_console.py b/reversing_tools/abbott/freestyle_hid_console.py deleted file mode 100755 index 18df89c..0000000 --- a/reversing_tools/abbott/freestyle_hid_console.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: © 2019 The glucometerutils Authors -# SPDX-License-Identifier: MIT -"""CLI tool to send messages through FreeStyle HID protocol.""" - -import argparse -import logging -import sys - -from glucometerutils import exceptions -from glucometerutils.support import freestyle - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument( - "--text_cmd_type", - action="store", - type=int, - default=0x60, - help="Message type for text commands sent to the device.", - ) - parser.add_argument( - "--text_reply_type", - action="store", - type=int, - default=0x60, - help="Message type for text replies received from the device.", - ) - parser.add_argument( - "device", action="store", help="Path to the HID device to open." - ) - - parser.add_argument( - "--vlog", - action="store", - required=False, - type=int, - help=( - "Python logging level. See the levels at " - "https://docs.python.org/3/library/logging.html#logging-levels" - ), - ) - - args = parser.parse_args() - - logging.basicConfig(level=args.vlog) - - session = freestyle.FreeStyleHidSession( - None, args.device, args.text_cmd_type, args.text_reply_type - ) - - session.connect() - - while True: - if sys.stdin.isatty(): - command = input(">>> ") - else: - command = input() - print(f">>> {command}") - - try: - print(session.send_text_command(bytes(command, "ascii"))) - except exceptions.InvalidResponse as error: - print(f"! {error}") - - -if __name__ == "__main__": - main() diff --git a/reversing_tools/abbott/known-commands.txt b/reversing_tools/abbott/known-commands.txt deleted file mode 100644 index be3f9f0..0000000 --- a/reversing_tools/abbott/known-commands.txt +++ /dev/null @@ -1,48 +0,0 @@ -$getrmndrst,0 -$getrmndr,0 -$rmdstrorder? -$actthm? -$wktrend? -$gunits? -$clktyp? -$alllang? -$lang? -$inslock? -$actinscal? -$iobstatus? -$foodunits? -$svgsdef? -$corsetup? -$insdose? -$inslog? -$inscalsetup? -$carbratio? -$svgsratio? -$mlcalget,3 -$cttype? -$bgdrop? -$bgtrgt? -$bgtgrng? -$ntsound? -$btsound? -$custthm? -$taglang? -$tagsenbl? -$tagorder? -$result? -$gettags,2,2 -$frststrt? -$marketlev? -$brandname? -$uom? -$temp? -$cksm? -$vrom? -$sn? -$serlnum? -$history? -$ptname? -$swver? -$date? -$time? -$ptid? diff --git a/reversing_tools/abbott/known-commands.txt.license b/reversing_tools/abbott/known-commands.txt.license deleted file mode 100644 index c662d53..0000000 --- a/reversing_tools/abbott/known-commands.txt.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2013 The glucometerutils Authors - -SPDX-License-Identifier: MIT diff --git a/setup.py b/setup.py index cbe985e..115a0ad 100644 --- a/setup.py +++ b/setup.py @@ -12,18 +12,16 @@ extras_require = { # listed as mandatory for the feature. "accucheck_reports": [], "contourusb": ["construct", "hidapi"], - "fsinsulinx": ["construct", "hidapi"], - "fslibre": ["construct", "hidapi"], + "fsinsulinx": ["freestyle_hid"], + "fslibre": ["freestyle_hid"], "fsoptium": ["pyserial"], - "fsprecisionneo": ["construct", "hidapi"], + "fsprecisionneo": ["freestyle_hid"], "otultra2": ["pyserial"], "otultraeasy": ["construct", "pyserial"], "otverio2015": ["construct", "PYSCSI[sgio]>=2.0.1"], "otverioiq": ["construct", "pyserial"], "sdcodefree": ["construct", "pyserial"], "td4277": ["construct", "pyserial[cp2110]>=3.5b0"], - # These are not drivers, but rather tools and features. - "reversing_tools": ["usbmon-tools"], "dev": [ "absl-py", "construct>=2.9", -- cgit v1.2.3