From b0584851f50a482a5af57752977b1c2df183a374 Mon Sep 17 00:00:00 2001 From: stres1 Date: Tue, 1 Oct 2024 13:08:49 +0200 Subject: [PATCH 1/8] stten1 --- .../instruction_images/st-ten-1/000825276.svg | 70 ++++++++++++++++++ .../instruction_images/st-ten-1/DEFAULT.svg | 49 ++++++++++++ .../st-ten-1/img/arw-yel-down.png | Bin 0 -> 5902 bytes config/instruction_images/st-ten-1/img/ok.png | Bin 0 -> 25455 bytes .../st-ten-1/img/tape_black.png | Bin 0 -> 49333 bytes .../st-ten-1/img/tape_white.png | Bin 0 -> 67353 bytes config/machine_settings/st-ten-1.ini | 5 +- .../barcode_recipe_selection.py | 2 +- 8 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 config/instruction_images/st-ten-1/000825276.svg create mode 100644 config/instruction_images/st-ten-1/DEFAULT.svg create mode 100644 config/instruction_images/st-ten-1/img/arw-yel-down.png create mode 100644 config/instruction_images/st-ten-1/img/ok.png create mode 100644 config/instruction_images/st-ten-1/img/tape_black.png create mode 100644 config/instruction_images/st-ten-1/img/tape_white.png diff --git a/config/instruction_images/st-ten-1/000825276.svg b/config/instruction_images/st-ten-1/000825276.svg new file mode 100644 index 0000000..03dcb32 --- /dev/null +++ b/config/instruction_images/st-ten-1/000825276.svg @@ -0,0 +1,70 @@ + + + + diff --git a/config/instruction_images/st-ten-1/DEFAULT.svg b/config/instruction_images/st-ten-1/DEFAULT.svg new file mode 100644 index 0000000..facc0bb --- /dev/null +++ b/config/instruction_images/st-ten-1/DEFAULT.svg @@ -0,0 +1,49 @@ + + + +DISEGNO NON DISPONIBILE diff --git a/config/instruction_images/st-ten-1/img/arw-yel-down.png b/config/instruction_images/st-ten-1/img/arw-yel-down.png new file mode 100644 index 0000000000000000000000000000000000000000..bb3c66855444adc75a7197c2b7f12f8d5a89fd7f GIT binary patch literal 5902 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 VhVE8GF!cvFpnFFjiPyA!`9B1EI?(_C literal 0 HcmV?d00001 diff --git a/config/instruction_images/st-ten-1/img/ok.png b/config/instruction_images/st-ten-1/img/ok.png new file mode 100644 index 0000000000000000000000000000000000000000..d576b7d109ab8e23290926df42e0d5362231e7e4 GIT binary patch literal 25455 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 B!^i*t literal 0 HcmV?d00001 diff --git a/config/instruction_images/st-ten-1/img/tape_black.png b/config/instruction_images/st-ten-1/img/tape_black.png new file mode 100644 index 0000000000000000000000000000000000000000..0d51c4b7e8f8f77e16f83b36e12387fb040bbaeb GIT binary patch literal 49333 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 WiqhL4 literal 0 HcmV?d00001 diff --git a/config/instruction_images/st-ten-1/img/tape_white.png b/config/instruction_images/st-ten-1/img/tape_white.png new file mode 100644 index 0000000000000000000000000000000000000000..d4b64f0c142915ac375c69db81d401b77b77ba68 GIT binary patch literal 67353 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

~|NuIoep+Sk6u=NFfc-S7X!hyTyT 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 rGUxO1$S)nQ%tX0: # RECIPE CODE FOUND self.recipe=candidates[-1] From 4b867dc7a96215a8283f36922eacac0d67746f60 Mon Sep 17 00:00:00 2001 From: stres1 Date: Fri, 4 Oct 2024 08:40:12 +0200 Subject: [PATCH 2/8] stten1 fix --- config/machine_settings/st-ten-1.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/machine_settings/st-ten-1.ini b/config/machine_settings/st-ten-1.ini index 6e370c6..b7d5d67 100644 --- a/config/machine_settings/st-ten-1.ini +++ b/config/machine_settings/st-ten-1.ini @@ -13,7 +13,7 @@ remote_api: absent tecna_t3: present vision_saver: absent vision: absent -screwdriver: absent +screwdriver: present fixture_id: present digital_io: present external_flush_blow: absent From 35a0cd9f81b2d28b1106aba28cc33e6ee5ef7b1b Mon Sep 17 00:00:00 2001 From: stres1 Date: Tue, 17 Dec 2024 12:29:43 +0100 Subject: [PATCH 3/8] stten1 cfg --- config/machine_settings/st-ten-1.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/machine_settings/st-ten-1.ini b/config/machine_settings/st-ten-1.ini index b7d5d67..8fbb92f 100644 --- a/config/machine_settings/st-ten-1.ini +++ b/config/machine_settings/st-ten-1.ini @@ -98,9 +98,9 @@ settling_pressure_min_percent: 5 settling_pressure_max_percent: 5 test_pressure: 7000 test_time: 10 -test_pressure_qpos: 22 #Q+ Upper test leak limit -test_pressure_qneg: 32 #Q- Lower test leak limit -test_pressure_tt_qpos: 1 # Q+ Upper test leak limit (tube-tube) +test_pressure_qpos: 5 #Q+ Upper test leak limit +test_pressure_qneg: 17 #Q- Lower test leak limit +test_pressure_tt_qpos: 5 # Q+ Upper test leak limit (tube-tube) test_pressure_tt_qneg: 5 # Q- Lower test leak limit (tube-tube) flush_time: 1 flush_pressure: 100 From b7a09646a907e6e6574cb88d56fe7b977552a8bd Mon Sep 17 00:00:00 2001 From: edo-neo Date: Wed, 18 Dec 2024 14:46:25 +0100 Subject: [PATCH 4/8] remote import recipes under testing --- src/components/archive_synchronizer.py | 60 ++++- src/ui/recipe_selection/recipe_selection.py | 265 ++------------------ 2 files changed, 69 insertions(+), 256 deletions(-) diff --git a/src/components/archive_synchronizer.py b/src/components/archive_synchronizer.py index 19eeeb5..2ee36a0 100644 --- a/src/components/archive_synchronizer.py +++ b/src/components/archive_synchronizer.py @@ -19,6 +19,8 @@ from PyQt5.QtCore import QThread from requests.adapters import HTTPAdapter, Retry from urllib3.exceptions import InsecureRequestWarning +from lib.helpers.recipe_manager import import_recipes, backup_current_recipes + from .component import Component from ui.helpers import get_main_window # Suppress insecure request warning @@ -135,6 +137,9 @@ class ArchiveSynchronizer(Component): return True def parse_response_and_execute(self, response): + """ + Parse the response and execute actions based on the `ACTIONS_TO_DO` received. + """ try: data = response.json() if not isinstance(data, dict): @@ -147,11 +152,53 @@ class ArchiveSynchronizer(Component): actions = [actions] for action in actions: - remote_path = action.get("remote_path") - local_path = action.get("local_path") - self.log.info(f"Executing remote fetch with remote_path: {remote_path} and local_path: {local_path}") - result = self.remote_fetch(remote_path=remote_path, local_path=local_path) - self.log.info(f"Remote fetch result: {result}") + action_type = action.get("action") # Determine which type of action to perform + + if action_type == "import": # Handle import action + remote_path = action.get("remote_path") + if not remote_path: + self.log.warning("Import action received without a remote_path.") + continue + + # Use remote_fetch to download the recipe file from the server + fetch_result = self.remote_fetch(remote_path=remote_path, local_path="tmp") + if 'downloaded_file' in fetch_result: + downloaded_file = fetch_result['downloaded_file'] + + self.log.info(f"Recipe file downloaded successfully to {downloaded_file}.") + + # Perform the import action + try: + # Backup current recipes before importing + backup_path = backup_current_recipes( + config=self.config, # Backup configuration object + logger=self.log # Logger for backup messages + ) + self.log.info(f"Backup created successfully at {backup_path}.") + + # Proceed with importing recipes + import_recipes( + config=self.config, + csv_path=downloaded_file, # Use the downloaded file path + logger=self.log + ) + self.log.info(f"Imported recipes successfully from {downloaded_file}.") + except Exception as e: + self.log.error(f"Failed to import recipes: {str(e)}") + continue + else: + self.log.warning(f"Failed to fetch the recipe file: {fetch_result.get('error')}.") + + elif action_type == "download": # Handle fetch action + remote_path = action.get("remote_path") + local_path = action.get("local_path", "tmp") # Use "tmp" as a fallback local path + self.log.info( + f"Executing remote fetch with remote_path: {remote_path} and local_path: {local_path}") + result = self.remote_fetch(remote_path=remote_path, local_path=local_path) + self.log.info(f"Remote fetch result: {result}") + + else: + self.log.warning(f"Unhandled action type: {action_type}") except json.JSONDecodeError: self.log.error("Failed to decode JSON response") @@ -283,8 +330,10 @@ class ArchiveSynchronizer(Component): self.log_to_db(log_time, log_info_type, log_info) return {"error": "Unexpected HTTP response status", "last_update_info": last_update_info} + # Make sure the local path exists os.makedirs(local_path, exist_ok=True) + # Construct the correct file path local_file_path = os.path.join(local_path, os.path.basename(remote_path)) with open(local_file_path, "wb") as f: f.write(response.content) @@ -292,6 +341,7 @@ class ArchiveSynchronizer(Component): log_info += f" - File downloaded successfully: {local_file_path}" self.log.info(log_info) self.log_to_db(log_time, log_info_type, log_info) + return {"downloaded_file": local_file_path, "last_update_info": last_update_info} except requests.ConnectionError as e: diff --git a/src/ui/recipe_selection/recipe_selection.py b/src/ui/recipe_selection/recipe_selection.py index 33f60ad..b5c4a26 100755 --- a/src/ui/recipe_selection/recipe_selection.py +++ b/src/ui/recipe_selection/recipe_selection.py @@ -10,6 +10,8 @@ from PyQt5.QtCore import QTimer, pyqtSignal from PyQt5.QtGui import QKeySequence from PyQt5.QtWidgets import QFileDialog, QMessageBox, QShortcut import shutil + +from lib.helpers.recipe_manager import export_recipes, import_recipes from ui.crud import Crud, Json_External_Dialog_Editor_Cell_Widget from ui.helpers import replace_widget from ui.recipe_spec_and_step_editor import Recipe_Spec_And_Step_Editor @@ -275,227 +277,22 @@ class Recipe_Selection(Widget): # IMPORT RECIPES FROM CSV FILE TO DATABASE def import_recipes(self, csv_path=None, defaults=None): - if defaults is None: - global noner - defaults = self.config.get("recipes_defaults", noner) - if csv_path is None: - options = QFileDialog.Options() - options |= QFileDialog.DontUseNativeDialog - csv_path, _ = QFileDialog.getOpenFileName( - self, - "Importazione ricette", - "ricette.csv", - "CSV data (*.csv);;All Files (*)", - options=options, - ) - csv_path = str(csv_path) - if not len(csv_path): - return - self.log.info(f"recipes: importing recipes from {csv_path}") - recipe_name_field = self.config.get("recipe", {}).get("recipe_name_field", "codice_ricetta").strip() - part_number_field = self.config.get("recipe", {}).get("part_number_field", "part_number").strip() - description_field = self.config.get("recipe", {}).get("description_field", "descrizione").strip() - barcode_enable_field = self.config.get("recipe", {}).get("barcode_enable_field", "verifica_codice_a_barre_abilitata").strip() - - with open(csv_path, "r", encoding="utf-8-sig") as f: - reader = csv.DictReader(f) - count = 0 - for ucrow in reader: - row = dict((k.lower(), v) for k, v in ucrow.items()) - recipe_name = row.get(recipe_name_field, defaults["codice_ricetta"]) - steps_specs = self.read_steps(row, defaults=defaults) - - # create recipe or update existing one in DB - try: - recipe = Recipes.get_by_id(recipe_name) - steps = recipe.get_steps_map() - recipe_is_new = False - except Recipes.DoesNotExist: - recipe = Recipes(name=recipe_name, part_number="TEMPORARY") - steps = {} - for step_name, step_spec in steps_specs.items(): - if step_name not in self.unsupported_steps: - steps[step_name] = step_spec - recipe_is_new = True - recipe.client = row.get("cliente", defaults["cliente"]) - recipe.part_number = row.get(part_number_field, defaults["part_number"]) - recipe.description = row.get(description_field, defaults["descrizione"]) - recipe.spec = { - "count": len(row.get("dimensione_lotto_abilitata", defaults["dimensione_lotto_abilitata"])) and "count" not in self.unsupported_steps, - "connector": len( - row.get("verifica_connettore_abilitata", defaults["verifica_connettore_abilitata"])) and "connector" not in self.unsupported_steps, - "barcodes": len(row.get(barcode_enable_field, defaults["verifica_codice_a_barre_abilitata"])) and "barcodes" not in self.unsupported_steps, - "resistance": len(row.get("verifica_resistenza_connettore_abilitata", - defaults["verifica_resistenza_connettore_abilitata"])) and "resistance" not in self.unsupported_steps, - "screws": len(row.get("avvitatura_abilitata", defaults["avvitatura_abilitata"])) and "screws" not in self.unsupported_steps, - "instruction": len(row.get("istruzione_abilitata", defaults["istruzione_abilitata"])) and "instruction" not in self.unsupported_steps, - "instruction_extra": len(row.get("istruzione_abilitata_extra", defaults["istruzione_abilitata_extra"])) and "instruction_extra" not in self.unsupported_steps, - "leak_1": len(row.get("prova_tenuta_abilitata", defaults["prova_tenuta_abilitata"])) and "leak_1" not in self.unsupported_steps, - "leak_2": len(row.get("prova_tenuta_abilitata_2", defaults["prova_tenuta_abilitata_2"])) and "leak_2" not in self.unsupported_steps, - "vision": len(row.get("test_visione_abilitato", defaults["test_visione_abilitato"])) and "vision" not in self.unsupported_steps, - "print": len(row.get("stampa_etichetta_abilitata", defaults["stampa_etichetta_abilitata"])) and "print" not in self.unsupported_steps, - "steps": steps, - } - recipe.spec["steps"]=steps_specs - if recipe_is_new: - recipe.save(force_insert=True) - else: - recipe.save() - count += 1 - db.commit() - self.log.info(f"recipes: imported {count} rows.") - self.crud.refresh() + import_recipes( + config=self.config, + csv_path=csv_path, + defaults=defaults, + unsupported_steps=self.unsupported_steps, + logger=self.log, + ) # EXPORT RECIPES TABLE TO CSV FILE def export_recipes(self, csv_path=None): - if csv_path is None: - options = QFileDialog.Options() - options |= QFileDialog.DontUseNativeDialog - csv_path, _ = QFileDialog.getSaveFileName( - self, - "Esportazione ricette", - "ricette.csv", - "CSV data (*.csv);;All Files (*)", - options=options, - ) - csv_path = str(csv_path) - if not len(csv_path): - return - if not csv_path.lower().endswith(".csv"): - csv_path += ".csv" - csv_dir = os.path.dirname(csv_path) - if len(csv_dir): - os.makedirs(csv_dir, exist_ok=True) - recipe_name_field = self.config.get("recipe", {}).get("recipe_name_field", "codice_ricetta").strip() - barcode_enable_field = self.config.get("recipe", {}).get("barcode_enable_field", "verifica_codice_a_barre_abilitata").strip() - barcode_serial_field = self.config.get("recipe", {}).get("barcode_serial_field", "codice_a_barre").strip() - print_template_field = self.config.get("recipe", {}).get("label_template_field", "modello_etichetta").strip() - data = [] - fieldnames = set() # Use a set to avoid duplicates - for recipe in list(Recipes.select()): - steps = recipe.get_steps_map() - exportable = { - # BASE SECTION - recipe_name_field: recipe.name, - "cliente": recipe.client, - "part_number": recipe.part_number, - } + export_recipes( + config=self.config, + csv_path=csv_path, + logger=self.log, + ) - # Add base fields to the fieldnames - fieldnames.update([recipe_name_field, "cliente", "part_number"]) - - # Check and add fields conditionally for each section - if "connector" in steps: - exportable.update({ - "verifica_connettore_abilitata": "x", - "connettore": steps["connector"].spec["connector"] - }) - fieldnames.update(["verifica_connettore_abilitata", "connettore"]) - - if "resistance" in steps: - exportable.update({ - "verifica_resistenza_connettore_abilitata": "x", - "scala_resistenza": steps["resistance"].spec["scale"], - "r nominale": steps["resistance"].spec["expected"], - "tolleranza_resistenza_pos": steps["resistance"].spec["tolerance_pos"], - "tolleranza_resistenza_neg": steps["resistance"].spec["tolerance_neg"] - }) - fieldnames.update(["verifica_resistenza_connettore_abilitata", "scala_resistenza", "r nominale", - "tolleranza_resistenza_pos", "tolleranza_resistenza_neg"]) - - if "barcodes" in steps: - exportable.update({ - barcode_enable_field: "x", - barcode_serial_field: steps["barcodes"].spec["serial"] - }) - fieldnames.update([barcode_enable_field, barcode_serial_field]) - - if recipe.spec.get("steps", {}).get("screws") and "screws" in steps: - exportable.update({ - "avvitatura_abilitata": "x", - "viti": steps["screws"].spec["quantity"] - }) - fieldnames.update(["avvitatura_abilitata", "viti"]) - - if "leak_1" in steps: - exportable.update({ - "prova_tenuta_abilitata": "x", - "tempo_pre_riempimento": steps["leak_1"].spec["pre_filling_time"], - "pressione_pre_riempimento": steps["leak_1"].spec["pre_filling_pressure"], - "tempo_riempimento": steps["leak_1"].spec["filling_time"], - "tempo_assestamento": steps["leak_1"].spec["settling_time"], - "percentuale_minima_pressione_assestamento": steps["leak_1"].spec["settling_pressure_min_percent"], - "percentuale_massima_pressione_assestamento": steps["leak_1"].spec["settling_pressure_max_percent"], - "tempo_di_test": steps["leak_1"].spec["test_time"], - "pressione_di_test_delta_minimo": steps["leak_1"].spec["test_pressure_qneg"], - "pressione_di_test": steps["leak_1"].spec["test_pressure"], - "pressione_di_test_delta_massimo": steps["leak_1"].spec["test_pressure_qpos"], - "tempo_svuotamento": steps["leak_1"].spec["flush_time"], - "pressione_svuotamento": steps["leak_1"].spec["flush_pressure"], - }) - fieldnames.update(["prova_tenuta_abilitata", "tempo_pre_riempimento", "pressione_pre_riempimento", - "tempo_riempimento", "tempo_assestamento", - "percentuale_minima_pressione_assestamento", - "percentuale_massima_pressione_assestamento", "tempo_di_test", - "pressione_di_test_delta_minimo", - "pressione_di_test", "pressione_di_test_delta_massimo", "tempo_svuotamento", - "pressione_svuotamento"]) - - if "leak_2" in steps: - exportable.update({ - "prova_tenuta_abilitata_2": "x", - "tempo_pre_riempimento_2": steps["leak_2"].spec["pre_filling_time"], - "pressione_pre_riempimento_2": steps["leak_2"].spec["pre_filling_pressure"], - "tempo_riempimento_2": steps["leak_2"].spec["filling_time"], - "tempo_assestamento_2": steps["leak_2"].spec["settling_time"], - "percentuale_minima_pressione_assestamento_2": steps["leak_2"].spec[ - "settling_pressure_min_percent"], - "percentuale_massima_pressione_assestamento_2": steps["leak_2"].spec[ - "settling_pressure_max_percent"], - "tempo_di_test_2": steps["leak_2"].spec["test_time"], - "pressione_di_test_delta_minimo_2": steps["leak_2"].spec["test_pressure_qneg"], - "pressione_di_test_2": steps["leak_2"].spec["test_pressure"], - "pressione_di_test_delta_massimo_2": steps["leak_2"].spec["test_pressure_qpos"], - "tempo_svuotamento_2": steps["leak_2"].spec["flush_time"], - "pressione_svuotamento_2": steps["leak_2"].spec["flush_pressure"], - }) - fieldnames.update(["prova_tenuta_abilitata_2", "tempo_pre_riempimento_2", "pressione_pre_riempimento_2", - "tempo_riempimento_2", "tempo_assestamento_2", - "percentuale_minima_pressione_assestamento_2", - "percentuale_massima_pressione_assestamento_2", "tempo_di_test_2", - "pressione_di_test_delta_minimo_2", "pressione_di_test_2", - "pressione_di_test_delta_massimo_2", - "tempo_svuotamento_2", "pressione_svuotamento_2"]) - - if "vision" in steps: - exportable.update({ - "test_visione_abilitato": recipe.spec["vision"], - "ricetta_visione": steps["vision"].spec["recipe"] - }) - fieldnames.update(["test_visione_abilitato", "ricetta_visione"]) - - if "print" in steps: - exportable.update({ - "stampa_etichetta_abilitata": "x", - print_template_field: steps["print"].spec["template"], - "etichette_supplementari": steps["print"].spec["extra_label"] - }) - fieldnames.update(["stampa_etichetta_abilitata", print_template_field, "etichette_supplementari"]) - - # Append the exportable dictionary to the data list - data.append(exportable) - - # Convert the set to a list for CSV writing - fieldnames = list(fieldnames) - - # Export to CSV if there is data - if len(data): - self.log.info(f"recipes: exporting recipes to {csv_path}") - with open(csv_path, "w", newline="") as f: - w = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore") - w.writeheader() - w.writerows(data) - self.log.info(f"recipes: exported {len(data)} rows.") def delete_recipes(self): ret = QMessageBox.warning( None, @@ -507,40 +304,6 @@ class Recipe_Selection(Widget): if ret == QMessageBox.Ok: Recipes.delete().execute() self.crud.refresh() - def backup_current_recipes(self): - # Define the backup directory and file name - backup_dir = os.path.join('config', 'csv_import', 'backup_csv') - timestamp = datetime.now().strftime("%d%m%y%H%M%S") - backup_file = f"backup_{timestamp}.csv" - backup_path = os.path.join(backup_dir, backup_file) - # Ensure the backup directory exists - os.makedirs(backup_dir, exist_ok=True) - # Export current recipes to backup file - self.export_recipes(csv_path=backup_path) - def move_imported_csv(self, csv_path): - # Move the imported CSV to the 'imported_csv' directory - imported_dir = os.path.join('config', 'csv_import', 'imported_csv') - os.makedirs(imported_dir, exist_ok=True) - imported_path = os.path.join(imported_dir, os.path.basename(csv_path)) - shutil.move(csv_path, imported_path) - self.log.info(f"Imported CSV moved to {imported_path}") - return imported_path - - def check_and_import_auto_csv(self): - # Define the directory to check - auto_import_dir = os.path.join('config', 'csv_import', 'auto_csv_import') - - # Check if the directory exists and is not empty - if os.path.exists(auto_import_dir) and os.listdir(auto_import_dir): - # Perform backup - self.backup_current_recipes() - - # Move and import each CSV file in the directory - for csv_file in os.listdir(auto_import_dir): - csv_path = os.path.join(auto_import_dir, csv_file) - if os.path.isfile(csv_path) and csv_file.endswith(".csv"): - self.import_recipes(csv_path=csv_path) - self.move_imported_csv(csv_path) From 270366af324dda75dce5f649fe7561b8300b273b Mon Sep 17 00:00:00 2001 From: neo-nb3 Date: Sun, 22 Dec 2024 18:39:13 +0100 Subject: [PATCH 5/8] fork nfcpy package as local lib, fixed pn532 baudrate, tbt --- src/lib/__init__.py | 1 + src/lib/nfc/__init__.py | 47 ++ src/lib/nfc/__main__.py | 214 +++++ src/lib/nfc/clf/__init__.py | 1251 ++++++++++++++++++++++++++++++ src/lib/nfc/clf/acr122.py | 242 ++++++ src/lib/nfc/clf/arygon.py | 105 +++ src/lib/nfc/clf/device.py | 660 ++++++++++++++++ src/lib/nfc/clf/pn531.py | 316 ++++++++ src/lib/nfc/clf/pn532.py | 454 +++++++++++ src/lib/nfc/clf/pn533.py | 399 ++++++++++ src/lib/nfc/clf/pn53x.py | 1064 +++++++++++++++++++++++++ src/lib/nfc/clf/rcs380.py | 986 +++++++++++++++++++++++ src/lib/nfc/clf/rcs956.py | 376 +++++++++ src/lib/nfc/clf/transport.py | 345 ++++++++ src/lib/nfc/clf/udp.py | 577 ++++++++++++++ src/lib/nfc/dep.py | 895 +++++++++++++++++++++ src/lib/nfc/handover/__init__.py | 29 + src/lib/nfc/handover/client.py | 118 +++ src/lib/nfc/handover/server.py | 128 +++ src/lib/nfc/llcp/__init__.py | 38 + src/lib/nfc/llcp/err.py | 42 + src/lib/nfc/llcp/llc.py | 886 +++++++++++++++++++++ src/lib/nfc/llcp/pdu.py | 945 ++++++++++++++++++++++ src/lib/nfc/llcp/sec.py | 542 +++++++++++++ src/lib/nfc/llcp/socket.py | 177 +++++ src/lib/nfc/llcp/tco.py | 733 +++++++++++++++++ src/lib/nfc/snep/__init__.py | 36 + src/lib/nfc/snep/client.py | 247 ++++++ src/lib/nfc/snep/server.py | 175 +++++ src/lib/nfc/tag/__init__.py | 480 ++++++++++++ src/lib/nfc/tag/tt1.py | 555 +++++++++++++ src/lib/nfc/tag/tt1_broadcom.py | 159 ++++ src/lib/nfc/tag/tt2.py | 697 +++++++++++++++++ src/lib/nfc/tag/tt2_nxp.py | 771 ++++++++++++++++++ src/lib/nfc/tag/tt3.py | 930 ++++++++++++++++++++++ src/lib/nfc/tag/tt3_sony.py | 987 +++++++++++++++++++++++ src/lib/nfc/tag/tt4.py | 579 ++++++++++++++ src/test/rfid.py | 4 +- 38 files changed, 17188 insertions(+), 2 deletions(-) create mode 100644 src/lib/__init__.py create mode 100644 src/lib/nfc/__init__.py create mode 100644 src/lib/nfc/__main__.py create mode 100644 src/lib/nfc/clf/__init__.py create mode 100644 src/lib/nfc/clf/acr122.py create mode 100644 src/lib/nfc/clf/arygon.py create mode 100644 src/lib/nfc/clf/device.py create mode 100644 src/lib/nfc/clf/pn531.py create mode 100644 src/lib/nfc/clf/pn532.py create mode 100644 src/lib/nfc/clf/pn533.py create mode 100644 src/lib/nfc/clf/pn53x.py create mode 100644 src/lib/nfc/clf/rcs380.py create mode 100644 src/lib/nfc/clf/rcs956.py create mode 100644 src/lib/nfc/clf/transport.py create mode 100644 src/lib/nfc/clf/udp.py create mode 100644 src/lib/nfc/dep.py create mode 100644 src/lib/nfc/handover/__init__.py create mode 100644 src/lib/nfc/handover/client.py create mode 100644 src/lib/nfc/handover/server.py create mode 100644 src/lib/nfc/llcp/__init__.py create mode 100644 src/lib/nfc/llcp/err.py create mode 100644 src/lib/nfc/llcp/llc.py create mode 100644 src/lib/nfc/llcp/pdu.py create mode 100644 src/lib/nfc/llcp/sec.py create mode 100644 src/lib/nfc/llcp/socket.py create mode 100644 src/lib/nfc/llcp/tco.py create mode 100644 src/lib/nfc/snep/__init__.py create mode 100644 src/lib/nfc/snep/client.py create mode 100644 src/lib/nfc/snep/server.py create mode 100644 src/lib/nfc/tag/__init__.py create mode 100644 src/lib/nfc/tag/tt1.py create mode 100644 src/lib/nfc/tag/tt1_broadcom.py create mode 100644 src/lib/nfc/tag/tt2.py create mode 100644 src/lib/nfc/tag/tt2_nxp.py create mode 100644 src/lib/nfc/tag/tt3.py create mode 100644 src/lib/nfc/tag/tt3_sony.py create mode 100644 src/lib/nfc/tag/tt4.py diff --git a/src/lib/__init__.py b/src/lib/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/lib/__init__.py @@ -0,0 +1 @@ + diff --git a/src/lib/nfc/__init__.py b/src/lib/nfc/__init__.py new file mode 100644 index 0000000..6d0c47d --- /dev/null +++ b/src/lib/nfc/__init__.py @@ -0,0 +1,47 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +from . import clf # noqa: F401 +from . import tag # noqa: F401 +from . import llcp # noqa: F401 +from . import snep # noqa: F401 +from . import handover # noqa: F401 +from .clf import ContactlessFrontend # noqa: F401 + +import logging +logging.getLogger(__name__).addHandler(logging.NullHandler()) +logging.getLogger(__name__).setLevel(logging.INFO) + +# METADATA #################################################################### + +__version__ = "1.0.4" + +__title__ = "nfcpy" +__description__ = "Python module for Near Field Communication." +__uri__ = "https://github.com/nfcpy/nfcpy" + +__author__ = "Stephen Tiedemann" +__email__ = "stephen.tiedemann@gmail.com" + +__license__ = "EUPL" +__copyright__ = "Copyright (c) 2009, 2019 Stephen Tiedemann" + +############################################################################### diff --git a/src/lib/nfc/__main__.py b/src/lib/nfc/__main__.py new file mode 100644 index 0000000..a5bc5a0 --- /dev/null +++ b/src/lib/nfc/__main__.py @@ -0,0 +1,214 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2016 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import nfc +import nfc.clf.device +import nfc.clf.transport + +import os +import errno +import logging +import platform +import argparse +import subprocess + +description = """ + +The nfcpy module implements a near field communication software stack +for reading and writing NFC Tags or peer-to-peer communication with +another NFC Device. It requires an NFC radio module connected through +either USB or serial interface. The nfcpy module is supposed to be +used within other applications, executing it as a module will try to +locate contactless devices connected to this machine. + +""" + + +def main(args): + print("This is the %s version of nfcpy run in Python %s\non %s" % + (nfc.__version__, platform.python_version(), platform.platform())) + print("I'm now searching your system for contactless devices") + + logging.basicConfig() + log_levels = (logging.WARN, logging.INFO, logging.DEBUG, logging.DEBUG-1) + log_level = log_levels[min(args.verbose, len(log_levels) - 1)] + logging.getLogger('nfc').setLevel(log_level) + + found = 0 + for vid, pid, bus, dev in nfc.clf.transport.USB.find("usb"): + if (vid, pid) in nfc.clf.device.usb_device_map: + path = "usb:{0:03d}:{1:03d}".format(bus, dev) + try: + clf = nfc.ContactlessFrontend(path) + print("** found %s" % clf.device) + clf.close() + found += 1 + except IOError as error: + if error.errno == errno.EACCES: + usb_device_access_denied(bus, dev, vid, pid, path) + elif error.errno == errno.EBUSY: + usb_device_found_is_busy(bus, dev, vid, pid, path) + + if args.search_tty: + for dev in nfc.clf.transport.TTY.find("tty")[0]: + path = "tty:{0}".format(dev[8:]) + try: + clf = nfc.ContactlessFrontend(path) + print("** found %s" % clf.device) + clf.close() + found += 1 + except IOError as error: + if error.errno == errno.EACCES: + print("access denied for device with path %s" % path) + elif error.errno == errno.EBUSY: + print("the device with path %s is busy" % path) + else: + print("I'm not trying serial devices because you haven't told me") + print("-- add the option '--search-tty' to have me looking") + print("-- but beware that this may break other serial devs") + + if not found: + print("Sorry, but I couldn't find any contactless device") + + +def usb_device_access_denied(bus, dev, vid, pid, path): + info = "** found usb:{vid:04x}:{pid:04x} at {path} but access is denied" + print(info.format(vid=vid, pid=pid, path=path)) + if platform.system().lower() == "linux": + devnode = "/dev/bus/usb/{0:03d}/{1:03d}".format(bus, dev) + if not os.access(devnode, os.R_OK | os.W_OK): + import pwd + import grp + usrname = pwd.getpwuid(os.getuid()).pw_name + devinfo = os.stat(devnode) + dev_usr = pwd.getpwuid(devinfo.st_uid).pw_name + dev_grp = grp.getgrgid(devinfo.st_gid).gr_name + try: + plugdev = grp.getgrnam("plugdev") + except KeyError: + plugdev = None + + udev_rule = 'SUBSYSTEM==\\"usb\\", ACTION==\\"add\\", ' \ + 'ATTRS{{idVendor}}==\\"{vid:04x}\\", ' \ + 'ATTRS{{idProduct}}==\\"{pid:04x}\\", ' \ + '{action}' + udev_file = "/etc/udev/rules.d/nfcdev.rules" + + print("-- the device is owned by '{dev_usr}' but you are '{user}'" + .format(dev_usr=dev_usr, user=usrname)) + print("-- also members of the '{dev_grp}' group would be permitted" + .format(dev_grp=dev_grp)) + print("-- you could use 'sudo' but this is not recommended") + + if plugdev is None: + print("-- it's better to adjust the device permissions") + action = 'MODE=\\"0666\\"' + udev_rule = udev_rule.format(vid=vid, pid=pid, action=action) + print(" sudo sh -c 'echo {udev_rule} >> {udev_file}'" + .format(udev_rule=udev_rule, udev_file=udev_file)) + print(" sudo udevadm control -R # then re-attach device") + elif dev_grp != "plugdev": + print("-- better assign the device to the 'plugdev' group") + action = 'GROUP=\\"plugdev\\"' + udev_rule = udev_rule.format(vid=vid, pid=pid, action=action) + print(" sudo sh -c 'echo {udev_rule} >> {udev_file}'" + .format(udev_rule=udev_rule, udev_file=udev_file)) + print(" sudo udevadm control -R # then re-attach device") + if usrname not in plugdev.gr_mem: + print("-- and make yourself member of the 'plugdev' group") + print(" sudo adduser {0} plugdev".format(usrname)) + print(" su - {0} # or logout once".format(usrname)) + elif usrname not in plugdev.gr_mem: + print("-- you should add yourself to the 'plugdev' group") + print(" sudo adduser {0} plugdev".format(usrname)) + print(" su - {0} # or logout once".format(usrname)) + else: + print("-- but unfortunately I have no better idea than that") + + +def usb_device_found_is_busy(bus, dev, vid, pid, path): + info = "** found usb:{vid:04x}:{pid:04x} at {path} but it's already used" + print(info.format(vid=vid, pid=pid, path=path)) + if platform.system().lower() == "linux": + sysfs = '/sys/bus/usb/devices/' + for entry in os.listdir(sysfs): + if not entry.startswith("usb") and ':' not in entry: + sysfs_device_entry = sysfs + entry + '/' + busnum = open(sysfs_device_entry + 'busnum').read().strip() + devnum = open(sysfs_device_entry + 'devnum').read().strip() + if int(busnum) == bus and int(devnum) == dev: + break + else: + print("-- impossible but nothing found in /sys/bus/usb/devices") + return + + # We now have the sysfs entry for the device in question. All + # supported contactless devices have a single configuration + # that will be listed if the device is used by another driver. + + blf = "/etc/modprobe.d/blacklist-nfc.conf" + sysfs_config_entry = sysfs_device_entry[:-1] + ":1.0/" + print("-- scan sysfs entry at '%s'" % sysfs_config_entry) + driver = os.readlink(sysfs_config_entry + "driver").split('/')[-1] + print("-- the device is used by the '%s' kernel driver" % driver) + if os.access(sysfs_config_entry + "nfc", os.F_OK): + print("-- this kernel driver belongs to the linux nfc subsystem") + print("-- you can remove it to free the device for this session") + print(" sudo modprobe -r %s" % driver) + print("-- and blacklist the driver to prevent loading next time") + print(" sudo sh -c 'echo blacklist %s >> %s'" % (driver, blf)) + elif driver == "usbfs": + print("-- this indicates a user mode driver with libusb") + devnode = "/dev/bus/usb/{0:03d}/{1:03d}".format(bus, dev) + print("-- find the process that uses " + devnode) + try: + subprocess.check_output("which lsof".split()) + except subprocess.CalledProcessError: + print("-- there is no 'lsof' command, can't help further") + else: + lsof = "lsof -t " + devnode + try: + pid = subprocess.check_output(lsof.split()).strip() + except subprocess.CalledProcessError: + pid = None + if pid is not None: + ps = "ps --no-headers -o cmd -p %s" % pid + cmd = subprocess.check_output(ps.split()).strip() + cwd = os.readlink("/proc/%s/cwd" % pid) + print("-- found that process %s uses the device" % pid) + print("-- process %s is '%s'" % (pid, cmd)) + print("-- in directory '%s'" % cwd) + else: + print(" ps --no-headers -o cmd -p `sudo %s`" % lsof) + + +parser = argparse.ArgumentParser( + prog="python -m nfc", description=description) + +parser.add_argument( + "--search-tty", action="store_true", + help="do also search for serial devices on linux") + +parser.add_argument( + "--verbose", "-v", action="count", default=0, + help="be verbose. Multiple -v options increase the verbosity.") + +main(parser.parse_args()) diff --git a/src/lib/nfc/clf/__init__.py b/src/lib/nfc/clf/__init__.py new file mode 100644 index 0000000..c6154dc --- /dev/null +++ b/src/lib/nfc/clf/__init__.py @@ -0,0 +1,1251 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- + +import src.lib.nfc.tag +import src.lib.nfc.dep +import src.lib.nfc.llcp +from . import device + +import binascii +import os +import re +import time +import errno +import threading + +import logging +log = logging.getLogger(__name__) + + +def print_data(data): + return 'None' if data is None else binascii.hexlify(data).decode('latin') + + +class ContactlessFrontend(object): + """This class is the main interface for working with contactless + devices. The :meth:`connect` method provides easy access to the + contactless functionality through automated discovery of remote + cards and devices and activation of appropiate upper level + protocols for further interaction. The :meth:`sense`, + :meth:`listen` and :meth:`exchange` methods provide a low-level + interface for more specialized tasks. + + An instance of the :class:`ContactlessFrontend` class manages a + single contactless device locally connect through either USB, TTY + or COM port. A special UDP port driver allows for emulation of a + contactless device that connects through UDP to another emulated + contactless device for test and development of higher layer + functions. + + A locally connected contactless device can be opened by either + supplying a *path* argument when an an instance of the contactless + frontend class is created or by calling :meth:`open` at a later + time. In either case the *path* argument must be constructed as + described in :meth:`open` and the same exceptions may occur. The + difference is that :meth:`open` returns False if a device could + not be found whereas the initialization method raises + :exc:`~exceptions.IOError` with :data:`errno.ENODEV`. + + The methods of the :class:`ContactlessFrontend` class are + thread-safe. + + """ + def __init__(self, path=None): + self.device = None + self.target = None + self.lock = threading.Lock() + if path and not self.open(path): + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + def open(self, path): + """Open a contactless reader identified by the search *path*. + + The :meth:`open` method searches and then opens a contactless + reader device for further communication. The *path* argument + can be flexibly constructed to identify more or less precisely + the device to open. A *path* that only partially identifies a + device is completed by search. The first device that is found + and successfully opened causes :meth:`open` to return True. If + no device is found return value is False. If a device was + found but could not be opened then :meth:`open` returns False + if *path* was partial or raise :exc:`~exceptions.IOError` if + *path* was fully qualified. Typical I/O error reasons are + :data:`errno.EACCES` if the calling process has insufficient + access rights or :data:`errno.EBUSY` if the device is used by + another process. + + A path is constructed as follows: + + ``usb[:vendor[:product]]`` + + with optional *vendor* and *product* as four digit + hexadecimal numbers. For example, ``usb:054c:06c3`` would + open the first Sony RC-S380 reader while ``usb:054c`` would + open the first Sony reader found on USB. + + ``usb[:bus[:device]]`` + + with optional *bus* and *device* number as three-digit + decimals. For example, ``usb:001:023`` would open the + device enumerated as number 23 on bus 1 while ``usb:001`` + would open the first device found on bust 1. Note that a + new device number is generated every time the device is + plugged into USB. Bus and device numbers are shown by + ``lsusb``. + + ``tty:port:driver`` + + with mandatory *port* and *driver* name. This is for Posix + systems to open the serial port ``/dev/tty`` and use + the driver module ``nfc/dev/.py`` for access. For + example, ``tty:USB0:arygon`` would open ``/dev/ttyUSB0`` + and load the Arygon APPx/ADRx driver. + + ``com:port:driver`` + + with mandatory *port* and *driver* name. This is for + Windows systems to open the serial port ``COM`` and + use the driver module ``nfc/dev/.py`` for access. + + ``udp[:host][:port]`` + + with optional *host* name or address and *port* + number. This will emulate a communication channel over + UDP/IP. The defaults for *host* and *port* are + ``localhost:54321``. + + """ + if not isinstance(path, str): + raise TypeError("expecting a string type argument *path*") + if not len(path) > 0: + raise ValueError("argument *path* must not be empty") + + # Close current device driver if this is not the first + # open. This allows to use several devices sequentially or + # re-initialize a device. + self.close() + + # Acquire the lock and search for a device on *path* + with self.lock: + log.info("searching for reader on path " + path) + self.device = device.connect(path) + if self.device: + log.info("using {0}".format(self.device)) + else: + log.error("no reader available on path " + path) + return bool(self.device) + + def close(self): + """Close the contacless reader device.""" + with self.lock: + if self.device is not None: + try: + self.device.close() + except IOError: + pass + self.device = None + + def connect(self, **options): + """Connect with a Target or Initiator + + The calling thread is blocked until a single activation and + deactivation has completed or a callback function supplied as + the keyword argument ``terminate`` returns a true value. The + example below makes :meth:`~connect()` return after 5 seconds, + regardless of whether a peer device was connected or not. + + >>> import nfc, time + >>> clf = nfc.ContactlessFrontend('usb') + >>> after5s = lambda: time.time() - started > 5 + >>> started = time.time(); clf.connect(llcp={}, terminate=after5s) + + Connect options are given as keyword arguments with dictionary + values. Possible options are: + + * ``rdwr={key: value, ...}`` - options for reader/writer + * ``llcp={key: value, ...}`` - options for peer to peer + * ``card={key: value, ...}`` - options for card emulation + + **Reader/Writer Options** + + 'targets' : iterable + A list of bitrate and technology type strings that will + produce the :class:`~nfc.clf.RemoteTarget` objects to + discover. The default is ``('106A', '106B', '212F')``. + + 'on-startup' : function(targets) + This function is called before any attempt to discover a + remote card. The *targets* argument provides a list of + :class:`RemoteTarget` objects prepared from the 'targets' + bitrate and technology type strings. The function must + return a list of of those :class:`RemoteTarget` objects + that shall be finally used for discovery, those targets may + have additional attributes. An empty list or anything else + that evaluates false will remove the 'rdwr' option + completely. + + 'on-discover' : function(target) + This function is called when a :class:`RemoteTarget` has + been discovered. The *target* argument contains the + technology type specific discovery responses and should be + evaluated for multi-protocol support. The target will be + further activated only if this function returns a true + value. The default function depends on the 'llcp' option, + if present then the function returns True only if the + target does not indicate peer to peer protocol support, + otherwise it returns True for all targets. + + 'on-connect' : function(tag) + This function is called when a remote tag has been + activated. The *tag* argument is an instance of class + :class:`nfc.tag.Tag` and can be used for tag reading and + writing within the callback or in a separate thread. Any + true return value instructs :meth:`connect` to wait until + the tag is no longer present and then return True, any + false return value implies immediate return of the + :class:`nfc.tag.Tag` object. + + 'on-release' : function(tag) + This function is called when the presence check was run + (the 'on-connect' function returned a true value) and + determined that communication with the *tag* has become + impossible, or when the 'terminate' function returned a + true value. The *tag* object may be used for cleanup + actions but not for communication. + + 'iterations' : integer + This determines the number of sense cycles performed + between calls to the terminate function. Each iteration + searches once for all specified targets. The default value + is 5 iterations and between each iteration is a waiting + time determined by the 'interval' option described below. + As an effect of math there will be no waiting time if + iterations is set to 1. + + 'interval' : float + This determines the waiting time between iterations. The + default value of 0.5 seconds is considered a sensible + tradeoff between responsiveness in terms of tag discovery + and power consumption. It should be clear that changing + this value will impair one or the other. There is no free + beer. + + 'beep-on-connect': boolean + If the device supports beeping or flashing an LED, + automatically perform this functionality when a tag is + successfully detected AND the 'on-connect' function + returns a true value. Defaults to True. + + .. sourcecode:: python + + import nfc + + def on_startup(targets): + for target in targets: + target.sensf_req = bytearray.fromhex("0012FC0000") + return targets + + def on_connect(tag): + print(tag) + + rdwr_options = { + 'targets': ['212F', '424F'], + 'on-startup': on_startup, + 'on-connect': on_connect, + } + with nfc.ContactlessFrontend('usb') as clf: + tag = clf.connect(rdwr=rdwr_options) + if tag.ndef: + print(tag.ndef.message.pretty()) + + **Peer To Peer Options** + + 'on-startup' : function(llc) + This function is called before any attempt to establish + peer to peer communication. The *llc* argument provides the + :class:`~nfc.llcp.llc.LogicalLinkController` that may be + used to allocate and bind listen sockets for local + services. The function should return the *llc* object if + activation shall continue. Any other value removes the + 'llcp' option. + + 'on-connect' : function(llc) + This function is called when peer to peer communication is + successfully established. The *llc* argument provides the + now activated :class:`~nfc.llcp.llc.LogicalLinkController` + ready for allocation of client communication sockets and + data exchange in separate work threads. The function should + a true value return more or less immediately, unless it + wishes to handle the logical link controller run loop by + itself and anytime later return a false value. + + 'on-release' : function(llc) + This function is called when the symmetry loop was run (the + 'on-connect' function returned a true value) and determined + that communication with the remote peer has become + impossible, or when the 'terminate' function returned a + true value. The *llc* object may be used for cleanup + actions but not for communication. + + 'role' : string + This attribute determines whether the local device will + restrict itself to either ``'initiator'`` or ``'target'`` + mode of operation. As Initiator the local device will try + to discover a remote device. As Target it waits for being + discovered. The default is to alternate between both roles. + + 'miu' : integer + This attribute sets the maximum information unit size that + is announced to the remote device during link activation. + The default and also smallest possible value is 128 bytes. + + 'lto' : integer + This attribute sets the link timeout value (given in + milliseconds) that is announced to the remote device during + link activation. It informs the remote device that if the + local device does not return a protocol data unit before + the timeout expires, the communication link is broken and + can not be recovered. The *lto* is an important part of the + user experience, it ultimately tells when the user should + no longer expect communication to continue. The default + value is 500 millisecond. + + 'agf' : boolean + Some early phone implementations did not properly handle + aggregated protocol data units. This attribute allows to + disable the use af aggregation at the cost of efficiency. + Aggregation is disabled with a false value. The default + is to use aggregation. + + 'brs' : integer + For the local device in Initiator role the bit rate + selector determines the the bitrate to negotiate with the + remote Target. The value may be 0, 1, or 2 for 106, 212, or + 424 kbps, respectively. The default is to negotiate 424 + kbps. + + 'acm' : boolean + For the local device in Initiator role this attribute + determines whether a remote Target may also be activated in + active communication mode. In active communication mode + both peer devices mutually generate a radio field when + sending. The default is to use passive communication mode. + + 'rwt' : float + For the local device in Target role this attribute sets the + response waiting time announced during link activation. The + response waiting time is a medium access layer (NFC-DEP) + value that indicates when the remote Initiator shall + attempt error recovery after missing a Target response. The + value is the waiting time index *wt* that determines the + effective response waiting time by the formula ``rwt = + 4096/13.56E6 * pow(2, wt)``. The value shall not be greater + than 14. The default value is 8 and yields an effective + response waiting time of 77.33 ms. + + 'lri' : integer + For the local device in Initiator role this attribute sets + the length reduction for medium access layer (NFC-DEP) + information frames. The value may be 0, 1, 2, or 3 for a + maximum payload size of 64, 128, 192, or 254 bytes, + respectively. The default value is 3. + + 'lrt' : integer + For the local device in Target role this attribute sets + the length reduction for medium access layer (NFC-DEP) + information frames. The value may be 0, 1, 2, or 3 for a + maximum payload size of 64, 128, 192, or 254 bytes, + respectively. The default value is 3. + + .. sourcecode:: python + + import nfc + import nfc.llcp + import threading + + def server(socket): + message, address = socket.recvfrom() + socket.sendto("It's me!", address) + socket.close() + + def client(socket): + socket.sendto("Hi there!", address=32) + socket.close() + + def on_startup(llc): + socket = nfc.llcp.Socket(llc, nfc.llcp.LOGICAL_DATA_LINK) + socket.bind(address=32) + threading.Thread(target=server, args=(socket,)).start() + return llc + + def on_connect(llc): + socket = nfc.llcp.Socket(llc, nfc.llcp.LOGICAL_DATA_LINK) + threading.Thread(target=client, args=(socket,)).start() + return True + + llcp_options = { + 'on-startup': on_startup, + 'on-connect': on_connect, + } + with nfc.ContactlessFrontend('usb') as clf: + clf.connect(llcp=llcp_options) + print("link terminated") + + **Card Emulation Options** + + 'on-startup' : function(target) + This function is called to prepare a local target for + discovery. The input argument is a fresh instance of an + unspecific :class:`LocalTarget` that can be set to the + desired bitrate and modulation type and populated with the + type specific discovery responses (see :meth:`listen` for + response data that is needed). The fully specified target + object must then be returned. + + 'on-discover' : function(target) + This function is called when the :class:`LocalTarget` has + been discovered. The *target* argument contains the + technology type specific discovery commands. The target + will be further activated only if this function returns a + true value. The default function always returns True. + + 'on-connect' : function(tag) + This function is called when the local target was + discovered and a :class:`nfc.tag.TagEmulation` object + successfully initialized. The function receives the + emulated *tag* object which stores the first command + received after inialization as ``tag.cmd``. The function + should return a true value if the tag.process_command() and + tag.send_response() methods shall be called repeatedly + until either the remote device terminates communication or + the 'terminate' function returns a true value. The function + should return a false value if the :meth:`connect` method + shall return immediately with the emulated *tag* object. + + 'on-release' : function(tag) + This function is called when the Target was released by the + Initiator or simply moved away, or if the terminate + callback function has returned a true value. The emulated + *tag* object may be used for cleanup actions but not for + communication. + + .. sourcecode:: python + + import nfc + + def on_startup(target): + idm = bytearray.fromhex("01010501b00ac30b") + pmm = bytearray.fromhex("03014b024f4993ff") + sys = bytearray.fromhex("1234") + target.brty = "212F" + target.sensf_res = chr(1) + idm + pmm + sys + return target + + def on_connect(tag): + print("discovered by remote reader") + return True + + def on_release(tag): + print("remote reader is gone") + return True + + card_options = { + 'on-startup': on_startup, + 'on-connect': on_connect, + 'on-release': on_release, + } + with nfc.ContactlessFrontend('usb') as clf: + clf.connect(card=card_options) + + **Return Value** + + The :meth:`connect` method returns :const:`None` if there were + no options left after the 'on-startup' functions have been + executed or when the 'terminate' function returned a true + value. It returns :const:`False` when terminated by any of the + following exceptions: :exc:`~exceptions.KeyboardInterrupt`, + :exc:`~exceptions.IOError`, :exc:`UnsupportedTargetError`. + + The :meth:`connect` method returns a :class:`~nfc.tag.Tag`, + :class:`~nfc.llcp.llc.LogicalLinkController`, or + :class:`~nfc.tag.TagEmulation` object if the associated + 'on-connect' function returned a false value to indicate that + it will handle presence check, peer to peer symmetry loop, or + command/response processing by itself. + + """ + if self.device is None: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + log.debug("connect{0}".format( + tuple([k for k in options if options[k]]))) + + terminate = options.get('terminate', lambda: False) + rdwr_options = options.get('rdwr') + llcp_options = options.get('llcp') + card_options = options.get('card') + + try: + assert isinstance(rdwr_options, (dict, type(None))), "rdwr" + assert isinstance(llcp_options, (dict, type(None))), "llcp" + assert isinstance(card_options, (dict, type(None))), "card" + except AssertionError as error: + raise TypeError("argument '%s' must be a dictionary" % error) + + if llcp_options is not None: + llcp_options = dict(llcp_options) + llcp_options.setdefault('on-startup', lambda llc: llc) + llcp_options.setdefault('on-connect', lambda llc: True) + llcp_options.setdefault('on-release', lambda llc: True) + + llc = nfc.llcp.llc.LogicalLinkController(**llcp_options) + llc = llcp_options['on-startup'](llc) + if isinstance(llc, nfc.llcp.llc.LogicalLinkController): + llcp_options['llc'] = llc + else: + log.debug("removing llcp_options after on-startup") + llcp_options = None + + if rdwr_options is not None: + def on_discover(target): + if target.sel_res and target.sel_res[0] & 0x40: + return False + elif target.sensf_res and target.sensf_res[1:3] == b"\x01\xFE": + return False + else: + return True + + rdwr_options = dict(rdwr_options) + rdwr_options.setdefault('targets', ['106A', '106B', '212F']) + rdwr_options.setdefault('on-startup', lambda targets: targets) + rdwr_options.setdefault('on-discover', on_discover) + rdwr_options.setdefault('on-connect', lambda tag: True) + rdwr_options.setdefault('on-release', lambda tag: True) + rdwr_options.setdefault('iterations', 5) + rdwr_options.setdefault('interval', 0.5) + rdwr_options.setdefault('beep-on-connect', True) + + targets = [RemoteTarget(brty) for brty in rdwr_options['targets']] + targets = rdwr_options['on-startup'](targets) + if targets and all([isinstance(o, RemoteTarget) for o in targets]): + rdwr_options['targets'] = targets + else: + log.debug("removing rdwr_options after on-startup") + rdwr_options = None + + if card_options is not None: + card_options = dict(card_options) + card_options.setdefault('on-startup', lambda target: None) + card_options.setdefault('on-discover', lambda target: True) + card_options.setdefault('on-connect', lambda tag: True) + card_options.setdefault('on-release', lambda tag: True) + + target = nfc.clf.LocalTarget() + target = card_options['on-startup'](target) + if isinstance(target, LocalTarget): + card_options['target'] = target + else: + log.debug("removing card_options after on-startup") + card_options = None + + if not (rdwr_options or llcp_options or card_options): + log.warning("no options to connect") + return None + + log.debug("connect options after startup: %s", + ', '.join(filter(bool, ["rdwr" if rdwr_options else None, + "llcp" if llcp_options else None, + "card" if card_options else None]))) + + try: + while not terminate(): + if rdwr_options: + result = self._rdwr_connect(rdwr_options, terminate) + if bool(result) is True: + return result + if llcp_options: + result = self._llcp_connect(llcp_options, terminate) + if bool(result) is True: + return result + if card_options: + result = self._card_connect(card_options, terminate) + if bool(result) is True: + return result + except IOError as error: + log.error(error) + return False + except UnsupportedTargetError as error: + log.info(error) + return False + except KeyboardInterrupt: + log.debug("terminated by keyboard interrupt") + return False + + def _rdwr_connect(self, options, terminate): + target = self.sense(*options['targets'], + iterations=options['iterations'], + interval=options['interval']) + if target is not None: + log.debug("discovered target {0}".format(target)) + if options['on-discover'](target): + tag = nfc.tag.activate(self, target) + if tag is not None: + log.debug("connected to {0}".format(tag)) + if options['on-connect'](tag): + if options['beep-on-connect']: + self.device.turn_on_led_and_buzzer() + while not terminate() and tag.is_present: + time.sleep(0.1) + self.device.turn_off_led_and_buzzer() + return options['on-release'](tag) + else: + return tag + + def _llcp_connect(self, options, terminate): + llc = options['llc'] + for role in ('target', 'initiator'): + if options.get('role') is None or options.get('role') == role: + DEP = eval("nfc.dep." + role.capitalize()) + dep_cfg = ('brs', 'acm', 'rwt', 'lrt', 'lri') + dep_cfg = {k: options[k] for k in dep_cfg if k in options} + if llc.activate(mac=DEP(clf=self), **dep_cfg): + log.debug("connected {0}".format(llc)) + if options['on-connect'](llc): + llc.run(terminate=terminate) + return options['on-release'](llc) + else: + return llc + + def _card_connect(self, options, terminate): + timeout = options.get('timeout', 1.0) + target = self.listen(options['target'], timeout) + if target and options['on-discover'](target): + log.debug("activated as {0}".format(target)) + tag = nfc.tag.emulate(self, target) + if isinstance(tag, nfc.tag.TagEmulation): + log.debug("connected as {0}".format(tag)) + if options['on-connect'](tag): + tag_rsp = tag.process_command(tag.cmd) + while not terminate(): + try: + tag_cmd = tag.send_response(tag_rsp, None) + tag_rsp = tag.process_command(tag_cmd) + except nfc.clf.BrokenLinkError as error: + log.debug(error) + break + except nfc.clf.CommunicationError as error: + log.debug(error) + tag_rsp = None + return options['on-release'](tag) + else: + return tag + + def sense(self, *targets, **options): + """Discover a contactless card or listening device. + + .. note:: The :meth:`sense` method is intended for experts + with a good understanding of the commands and + responses exchanged during target activation (the + notion used for commands and responses follows the + NFC Forum Digital Specification). If the greater + level of control is not needed it is recommended to + use the :meth:`connect` method. + + All positional arguments build the list of potential *targets* + to discover and must be of type :class:`RemoteTarget`. Keyword + argument *options* may be the number of ``iterations`` of the + sense loop set by *targets* and the ``interval`` between + iterations. The return value is either a :class:`RemoteTarget` + instance or :const:`None`. + + >>> import nfc, nfc.clf + >>> from binascii import hexlify + >>> clf = nfc.ContactlessFrontend("usb") + >>> target1 = nfc.clf.RemoteTarget("106A") + >>> target2 = nfc.clf.RemoteTarget("212F") + >>> print(clf.sense(target1, target2, iterations=5, interval=0.2)) + 106A(sdd_res=04497622D93881, sel_res=00, sens_res=4400) + + A **Type A Target** is specified with the technology letter + ``A`` following the bitrate to be used for the SENS_REQ + command (almost always must the bitrate be 106 kbps). To + discover only a specific Type A target, the NFCID1 (UID) can + be set with a 4, 7, or 10 byte ``sel_req`` attribute (cascade + tags are handled internally). + + >>> target = nfc.clf.RemoteTarget("106A") + >>> print(clf.sense(target)) + 106A sdd_res=04497622D93881 sel_res=00 sens_res=4400 + >>> target.sel_req = bytearray.fromhex("04497622D93881") + >>> print(clf.sense(target)) + 106A sdd_res=04497622D93881 sel_res=00 sens_res=4400 + >>> target.sel_req = bytearray.fromhex("04497622") + >>> print(clf.sense(target)) + None + + A **Type B Target** is specified with the technology letter + ``B`` following the bitrate to be used for the SENSB_REQ + command (almost always must the bitrate be 106 kbps). A + specific application family identifier can be set with the + first byte of a ``sensb_req`` attribute (the second byte PARAM + is ignored when it can not be set to local device, 00h is a + safe value in all cases). + + >>> target = nfc.clf.RemoteTarget("106B") + >>> print(clf.sense(target)) + 106B sens_res=50E5DD3DC900000011008185 + >>> target.sensb_req = bytearray.fromhex("0000") + >>> print(clf.sense(target)) + 106B sens_res=50E5DD3DC900000011008185 + >>> target.sensb_req = bytearray.fromhex("FF00") + >>> print(clf.sense(target)) + None + + A **Type F Target** is specified with the technology letter + ``F`` following the bitrate to be used for the SENSF_REQ + command (the typically supported bitrates are 212 and 424 + kbps). The default SENSF_REQ command allows all targets to + answer, requests system code information, and selects a single + time slot for the SENSF_RES response. This can be changed with + the ``sensf_req`` attribute. + + >>> target = nfc.clf.RemoteTarget("212F") + >>> print(clf.sense(target)) + 212F sensf_res=0101010601B00ADE0B03014B024F4993FF12FC + >>> target.sensf_req = bytearray.fromhex("0012FC0000") + >>> print(clf.sense(target)) + 212F sensf_res=0101010601B00ADE0B03014B024F4993FF + >>> target.sensf_req = bytearray.fromhex("00ABCD0000") + >>> print(clf.sense(target)) + None + + An **Active Communication Mode P2P Target** search is selected + with an ``atr_req`` attribute. The choice of bitrate and + modulation type is 106A, 212F, and 424F. + + >>> atr = bytearray.fromhex("D4000102030405060708091000000030") + >>> target = clf.sense(nfc.clf.RemoteTarget("106A", atr_req=atr)) + >>> if target and target.atr_res: + >>> print(hexlify(target.atr_res).decode()) + d501c023cae6b3182afe3dee0000000e3246666d01011103020013040196 + >>> target = clf.sense(nfc.clf.RemoteTarget("424F", atr_req=atr)) + >>> if target and target.atr_res: + >>> print(hexlify(target.atr_res).decode()) + d501dc0104f04584e15769700000000e3246666d01011103020013040196 + + Some drivers must modify the ATR_REQ to cope with hardware + limitations, for example change length reduction value to + reduce the maximum size of target responses. The ATR_REQ that + has been send is given by the ``atr_req`` attribute of the + returned RemoteTarget object. + + A **Passive Communication Mode P2P Target** responds to 106A + discovery with bit 6 of SEL_RES set to 1, and to 212F/424F + discovery (when the request code RC is 0 in the SENSF_REQ + command) with an NFCID2 that starts with 01FEh in the + SENSF_RES response. Responses below are from a Nexus 5 + configured for NFC-DEP Protocol (SEL_RES bit 6 is set) and + Type 4A Tag (SEL_RES bit 5 is set). + + >>> print(clf.sense(nfc.clf.RemoteTarget("106A"))) + 106A sdd_res=08796BEB sel_res=60 sens_res=0400 + >>> sensf_req = bytearray.fromhex("00FFFF0000") + >>> print(clf.sense(nfc.clf.RemoteTarget("424F", sensf_req=sensf_req))) + 424F sensf_res=0101FE1444EFB88FD50000000000000000 + + Errors found in the *targets* argument list raise exceptions + only if exactly one target is given. If multiple targets are + provided, any target that is not supported or has invalid + attributes is just ignored (but is logged as a debug message). + + **Exceptions** + + * :exc:`~exceptions.IOError` (ENODEV) when a local contacless + communication device has not been opened or communication + with the local device is no longer possible. + + * :exc:`nfc.clf.UnsupportedTargetError` if the single target + supplied as input is not supported by the active driver. + This exception is never raised when :meth:`sense` is called + with multiple targets, those unsupported are then silently + ignored. + + """ + def sense_tta(target): + if target.sel_req and len(target.sel_req) not in (4, 7, 10): + raise ValueError("sel_req must be 4, 7, or 10 byte") + target = self.device.sense_tta(target) + log.debug("found %s", target) + if target and len(target.sens_res) != 2: + error = "SENS Response Format Error (wrong length)" + log.debug(error) + raise ProtocolError(error) + if target and target.sens_res[0] & 0b00011111 == 0: + if target.sens_res[1] & 0b00001111 != 0b1100: + error = "SENS Response Data Error (T1T config)" + log.debug(error) + raise ProtocolError(error) + if not target.rid_res: + error = "RID Response Error (no response received)" + log.debug(error) + raise ProtocolError(error) + if len(target.rid_res) != 6: + error = "RID Response Format Error (wrong length)" + log.debug(error) + raise ProtocolError(error) + if target.rid_res[0] >> 4 != 0b0001: + error = "RID Response Data Error (invalid HR0)" + log.debug(error) + raise ProtocolError(error) + return target + + def sense_ttb(target): + return self.device.sense_ttb(target) + + def sense_ttf(target): + return self.device.sense_ttf(target) + + def sense_dep(target): + if len(target.atr_req) < 16: + raise ValueError("minimum atr_req length is 16 byte") + if len(target.atr_req) > 64: + raise ValueError("maximum atr_req length is 64 byte") + return self.device.sense_dep(target) + + for target in targets: + if not isinstance(target, RemoteTarget): + raise ValueError("invalid target argument type: %r" % target) + + with self.lock: + if self.device is None: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + self.target = None # forget captured target + self.device.mute() # deactivate the rf field + + for i in range(max(1, options.get('iterations', 1))): + started = time.time() + for target in targets: + log.debug("sense {0}".format(target)) + try: + if target.atr_req is not None: + self.target = sense_dep(target) + elif target.brty.endswith('A'): + self.target = sense_tta(target) + elif target.brty.endswith('B'): + self.target = sense_ttb(target) + elif target.brty.endswith('F'): + self.target = sense_ttf(target) + else: + info = "unknown technology type in %r" + raise UnsupportedTargetError(info % target.brty) + except UnsupportedTargetError as error: + if len(targets) == 1: + raise error + else: + log.debug(error) + except CommunicationError as error: + log.debug(error) + else: + if self.target is not None: + log.debug("found {0}".format(self.target)) + return self.target + if len(targets) > 0: + self.device.mute() # deactivate the rf field + if i < options.get('iterations', 1) - 1: + elapsed = time.time() - started + time.sleep(max(0, options.get('interval', 0.1)-elapsed)) + + def listen(self, target, timeout): + """Listen *timeout* seconds to become activated as *target*. + + .. note:: The :meth:`listen` method is intended for experts + with a good understanding of the commands and + responses exchanged during target activation (the + notion used for commands and responses follows the + NFC Forum Digital Specification). If the greater + level of control is not needed it is recommended to + use the :meth:`connect` method. + + The *target* argument is a :class:`LocalTarget` object that + provides bitrate, technology type and response data + attributes. The return value is either a :class:`LocalTarget` + object with bitrate, technology type and request/response data + attributes or :const:`None`. + + An **P2P Target** is selected when the ``atr_res`` attribute + is set. The bitrate and technology type are decided by the + Initiator and do not need to be specified. The ``sens_res``, + ``sdd_res`` and ``sel_res`` attributes for Type A technology + as well as the ``sensf_res`` attribute for Type F technolgy + must all be set. + + When activated, the bitrate and type are set to the current + communication values, the ``atr_req`` attribute contains the + ATR_REQ received from the Initiator and the ``dep_req`` + attribute contains the first DEP_REQ received after + activation. If the Initiator has changed communication + parameters, the ``psl_req`` attribute holds the PSL_REQ that + was received. The ``atr_res`` (and the ``psl_res`` if + transmitted) are also made available. + + If the local target was activated in passive communication + mode either the Type A response (``sens_res``, ``sdd_res``, + ``sel_res``) or Type F response (``sensf_res``) attributes + will be present. + + With a Nexus 5 on a reader connected via USB the following + code should be working and produce similar output (the Nexus 5 + prioritizes active communication mode): + + >>> import nfc, nfc.clf + >>> clf = nfc.ContactlessFrontend("usb") + >>> atr_res = "d50101fe0102030405060708000000083246666d010110" + >>> target = nfc.clf.LocalTarget() + >>> target.sensf_res = bytearray.fromhex("0101FE"+16*"FF") + >>> target.sens_res = bytearray.fromhex("0101") + >>> target.sdd_res = bytearray.fromhex("08010203") + >>> target.sel_res = bytearray.fromhex("40") + >>> target.atr_res = bytearray.fromhex(atr_res) + >>> print(clf.listen(target, timeout=2.5)) + 424F atr_res=D50101FE0102030405060708000000083246666D010110 ... + + A **Type A Target** is selected when ``atr_res`` is not + present and the technology type is ``A``. The bitrate should + be set to 106 kbps, even if a driver supports higher bitrates + they would need to be set after activation. The ``sens_res``, + ``sdd_res`` and ``sel_res`` attributes must all be provided. + + >>> target = nfc.clf.Localtarget("106A") + >>> target.sens_res = bytearray.fromhex("0101")) + >>> target.sdd_res = bytearray.fromhex("08010203") + >>> target.sel_res = bytearray.fromhex("00") + >>> print(clf.listen(target, timeout=2.5)) + 106A sdd_res=08010203 sel_res=00 sens_res=0101 tt2_cmd=3000 + + A **Type B Target** is selected when ``atr_res`` is not + present and the technology type is ``B``. Unfortunately none + of the supported devices supports Type B technology for listen + and an :exc:`nfc.clf.UnsupportedTargetError` exception will be + the only result. + + >>> target = nfc.clf.LocalTarget("106B") + >>> try: clf.listen(target, 2.5) + ... except nfc.clf.UnsupportedTargetError: print("sorry") + ... + sorry + + A **Type F Target** is selected when ``atr_res`` is not + present and the technology type is ``F``. The bitrate may be + 212 or 424 kbps. The ``sensf_res`` attribute must be provided. + + >>> idm, pmm, sys = "02FE010203040506", "FFFFFFFFFFFFFFFF", "12FC" + >>> target = nfc.clf.LocalTarget("212F") + >>> target.sensf_res = bytearray.fromhex("01" + idm + pmm + sys) + >>> print(clf.listen(target, 2.5)) + 212F sensf_req=00FFFF0003 tt3_cmd=0C02FE010203040506 ... + + **Exceptions** + + * :exc:`~exceptions.IOError` (ENODEV) when a local contacless + communication device has not been opened or communication + with the local device is no longer possible. + + * :exc:`nfc.clf.UnsupportedTargetError` if the single target + supplied as input is not supported by the active driver. + This exception is never raised when :meth:`sense` is called + with multiple targets, those unsupported are then silently + ignored. + + """ + def listen_tta(target, timeout): + return self.device.listen_tta(target, timeout) + + def listen_ttb(target, timeout): + return self.device.listen_ttb(target, timeout) + + def listen_ttf(target, timeout): + return self.device.listen_ttf(target, timeout) + + def listen_dep(target, timeout): + target = self.device.listen_dep(target, timeout) + if target and target.atr_req: + try: + assert len(target.atr_req) >= 16, "less than 16 byte" + assert len(target.atr_req) <= 64, "more than 64 byte" + return target + except AssertionError as error: + log.debug("atr_req is %s", str(error)) + + assert isinstance(target, LocalTarget), \ + "invalid target argument type: %r" % target + + with self.lock: + if self.device is None: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + self.target = None # forget captured target + self.device.mute() # deactivate the rf field + + info = "listen %.3f seconds for %s" + if target.atr_res is not None: + log.debug(info, timeout, "DEP") + self.target = listen_dep(target, timeout) + elif target.brty in ('106A', '212A', '424A'): + log.debug(info, timeout, target) + self.target = listen_tta(target, timeout) + elif target.brty in ('106B', '212B', '424B', '848B'): + log.debug(info, timeout, target) + self.target = listen_ttb(target, timeout) + elif target.brty in ('212F', '424F'): + log.debug(info, timeout, target) + self.target = listen_ttf(target, timeout) + else: + errmsg = "unsupported bitrate technology type {}" + raise ValueError(errmsg.format(target.brty)) + + return self.target + + def exchange(self, send_data, timeout): + """Exchange data with an activated target (*send_data* is a command + frame) or as an activated target (*send_data* is a response + frame). Returns a target response frame (if data is send to an + activated target) or a next command frame (if data is send + from an activated target). Returns None if the communication + link broke during exchange (if data is sent as a target). The + timeout is the number of seconds to wait for data to return, + if the timeout expires an nfc.clf.TimeoutException is + raised. Other nfc.clf.CommunicationError exceptions may be raised if + an error is detected during communication. + + """ + with self.lock: + if self.device is None: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + log.debug(">>> %s timeout=%s", print_data(send_data), str(timeout)) + + if isinstance(self.target, RemoteTarget): + exchange = self.device.send_cmd_recv_rsp + elif isinstance(self.target, LocalTarget): + exchange = self.device.send_rsp_recv_cmd + else: + log.error("no target for data exchange") + return None + + send_time = time.time() + rcvd_data = exchange(self.target, send_data, timeout) + recv_time = time.time() - send_time + + log.debug("<<< %s %.3fs", print_data(rcvd_data), recv_time) + return rcvd_data + + @property + def max_send_data_size(self): + """The maximum number of octets that can be send with the + :meth:`exchange` method in the established operating mode. + + """ + with self.lock: + if self.device is None: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + else: + return self.device.get_max_send_data_size(self.target) + + @property + def max_recv_data_size(self): + """The maximum number of octets that can be received with the + :meth:`exchange` method in the established operating mode. + + """ + with self.lock: + if self.device is None: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + else: + return self.device.get_max_recv_data_size(self.target) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def __str__(self): + if self.device is not None: + s = "{dev.vendor_name} {dev.product_name} on {dev.path}" + return s.format(dev=self.device) + else: + return self.__repr__() + + +############################################################################### +# +# Targets +# +############################################################################### +class Target(object): + def __init__(self, **kwargs): + for name in kwargs: + self.__dict__[name] = kwargs[name] + + def __getattr__(self, name): + return None + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + def __str__(self): + attrs = [] + for name in sorted(self.__dict__.keys()): + if name.startswith('_'): + continue + value = self.__dict__[name] + if isinstance(value, (bytes, bytearray)): + value = binascii.hexlify(value).decode().upper() + attrs.append("{0}={1}".format(name, value)) + return "{brty} {attrs}".format(brty=self.brty, attrs=' '.join(attrs)) + + +class RemoteTarget(Target): + """A RemoteTarget instance provides bitrate and technology type and + command/response data of a remote card or device that, when input + to :meth:`sense`, shall be attempted to discover and, when + returned by :meth:`sense`, has been discovered by the local + device. Command/response data attributes, whatever name, default + to None. + + """ + brty_pattern = re.compile(r'(\d+[A-Z])(?:/(\d+[A-Z])|.*)') + + def __init__(self, brty, **kwargs): + super(RemoteTarget, self).__init__(**kwargs) + self.brty = brty + + @property + def brty(self): + """A string that combines bitrate and technology type, e.g. '106A'.""" + return self._brty_send + + @brty.setter + def brty(self, value): + brty_pattern_match = self.brty_pattern.match(value) + if brty_pattern_match: + (self._brty_send, self._brty_recv) = brty_pattern_match.groups() + if not self._brty_recv: + self._brty_recv = self._brty_send + else: + raise ValueError("brty pattern does not match for %r" % value) + + @property + def brty_send(self): + return self._brty_send + + @property + def brty_recv(self): + return self._brty_recv + + +class LocalTarget(Target): + """A LocalTarget instance provides bitrate and technology type and + command/response data of the local card or device that, when input + to :meth:`listen`, shall be made available for discovery and, when + returned by :meth:`listen`, has been discovered by a remote + device. Command/response data attributes, whatever name, default + to None. + + """ + def __init__(self, brty='106A', **kwargs): + super(LocalTarget, self).__init__(**kwargs) + self.brty = brty + + @property + def brty(self): + """A string that combines bitrate and technology type, e.g. '106A'.""" + return self._brty_send \ + if self._brty_send == self._brty_recv \ + else self._brty_send+"/"+self._brty_recv + + @brty.setter + def brty(self, value): + self._brty_send = self._brty_recv = value + + +############################################################################### +# +# Exceptions +# +############################################################################### +class Error(Exception): + """Base class for exceptions specific to the contacless frontend module. + + - UnsupportedTargetError + - CommunicationError + + - ProtocolError + - TransmissionError + - TimeoutError + - BrokenLinkError + + """ + + +class UnsupportedTargetError(Error): + """The :class:`RemoteTarget` input to + :meth:`ContactlessFrontend.sense` or :class:`LocalTarget` input to + :meth:`ContactlessFrontend.listen` is not supported by the local + device. + + """ + + +class CommunicationError(Error): + """Base class for communication errors. + + """ + + +class ProtocolError(CommunicationError): + """Raised when an NFC Forum Digital Specification protocol error + occured. + + """ + + +class TransmissionError(CommunicationError): + """Raised when an NFC Forum Digital Specification transmission error + occured. + + """ + + +class TimeoutError(CommunicationError): + """Raised when an NFC Forum Digital Specification timeout error + occured. + + """ + + +class BrokenLinkError(CommunicationError): + """The remote device (Reader/Writer or P2P Device) has deactivated the + RF field or is no longer within communication distance. + + """ diff --git a/src/lib/nfc/clf/acr122.py b/src/lib/nfc/clf/acr122.py new file mode 100644 index 0000000..54b647b --- /dev/null +++ b/src/lib/nfc/clf/acr122.py @@ -0,0 +1,242 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2011, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""Device driver for the Arygon ACR122U contactless reader. + +The Arygon ACR122U is a PC/SC compliant contactless reader that +connects via USB and uses the USB CCID profile. It is normally +intented to be used with a PC/SC stack but this driver interfaces +directly with the inbuilt PN532 chipset by tunneling commands through +the PC/SC Escape command. The driver is limited in functionality +because the embedded microprocessor (that implements the PC/SC stack) +also operates the PN532; it does not allow all commands to pass as +desired and reacts on chip responses with its own (legitimate) +interpretation of state. + +========== ======= ============ +function support remarks +========== ======= ============ +sense_tta yes Type 1 (Topaz) Tags are not supported +sense_ttb yes ATTRIB by firmware voided with S(DESELECT) +sense_ttf yes +sense_dep yes +listen_tta no +listen_ttb no +listen_ttf no +listen_dep no +========== ======= ============ + +""" +import nfc.clf +from . import pn532 + +import os +import errno +import struct +from binascii import hexlify + +import logging +log = logging.getLogger(__name__) + + +def init(transport): + device = Device(Chipset(transport)) + device._vendor_name = transport.manufacturer_name + device._device_name = transport.product_name.split()[0] + return device + + +class Device(pn532.Device): + # Device driver class for the ACR122U. + + def __init__(self, chipset): + super(Device, self).__init__(chipset, logger=log) + + def sense_tta(self, target): + """Activate the RF field and probe for a Type A Target at 106 + kbps. Other bitrates are not supported. Type 1 Tags are not + supported because the device does not allow to send the + correct RID command (even though the PN532 does). + + """ + return super(Device, self).sense_tta(target) + + def sense_ttb(self, target): + """Activate the RF field and probe for a Type B Target. + + The RC-S956 can discover Type B Targets (Type 4B Tag) at 106 + kbps. For a Type 4B Tag the firmware automatically sends an + ATTRIB command that configures the use of DID and 64 byte + maximum frame size. The driver reverts this configuration with + a DESELECT and WUPB command to return the target prepared for + activation (which nfcpy does in the tag activation code). + + """ + return super(Device, self).sense_ttb(target) + + def sense_ttf(self, target): + """Activate the RF field and probe for a Type F Target. Bitrates 212 + and 424 kpbs are supported. + + """ + return super(Device, self).sense_ttf(target) + + def sense_dep(self, target): + """Search for a DEP Target. Both passive and passive communication + mode are supported. + + """ + return super(Device, self).sense_dep(target) + + def listen_tta(self, target, timeout): + """Listen as Type A Target is not supported.""" + info = "{device} does not support listen as Type A Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_ttb(self, target, timeout): + """Listen as Type B Target is not supported.""" + info = "{device} does not support listen as Type B Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_ttf(self, target, timeout): + """Listen as Type F Target is not supported.""" + info = "{device} does not support listen as Type F Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_dep(self, target, timeout): + """Listen as DEP Target is not supported.""" + info = "{device} does not support listen as DEP Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def turn_on_led_and_buzzer(self): + """Buzz and turn red.""" + self.chipset.set_buzzer_and_led_to_active() + + def turn_off_led_and_buzzer(self): + """Back to green.""" + self.chipset.set_buzzer_and_led_to_default() + + +class Chipset(pn532.Chipset): + # Maximum size of a host command frame to the contactless chip. + host_command_frame_max_size = 254 + + # Supported BrTy (baud rate / modulation type) values for the + # InListPassiveTarget command. Corresponds to 106 kbps Type A, 212 + # kbps Type F, 424 kbps Type F, and 106 kbps Type B. The value for + # 106 kbps Innovision Jewel Tag (although supported by PN532) is + # removed because the RID command can not be send. + in_list_passive_target_brty_range = (0, 1, 2, 3) + + def __init__(self, transport): + self.transport = transport + + # read ACR122U firmware version string + reader_version = self.ccid_xfr_block(bytearray.fromhex("FF00480000")) + if not reader_version.startswith(b"ACR122U"): + log.error("failed to retrieve ACR122U version string") + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + if int(chr(reader_version[7])) < 2: + log.error("{0} not supported, need 2.x".format(reader_version[7:])) + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + log.debug("initialize " + reader_version.decode()) + + # set icc power on + log.debug("CCID ICC-POWER-ON") + frame = bytearray.fromhex("62000000000000000000") + transport.write(frame) + transport.read(100) + + # disable autodetection + log.debug("Set PICC Operating Parameters") + self.ccid_xfr_block(bytearray.fromhex("FF00517F00")) + + # switch red/green led off/on + log.debug("Configure Buzzer and LED") + self.set_buzzer_and_led_to_default() + + super(Chipset, self).__init__(transport, logger=log) + + def close(self): + self.ccid_xfr_block(bytearray.fromhex("FF00400C0400000000")) + self.transport.close() + self.transport = None + + def set_buzzer_and_led_to_default(self): + """Turn off buzzer and set LED to default (green only). """ + self.ccid_xfr_block(bytearray.fromhex("FF00400E0400000000")) + + def set_buzzer_and_led_to_active(self, duration_in_ms=300): + """Turn on buzzer and set LED to red only. The timeout here must exceed + the total buzzer/flash duration defined in bytes 5-8. """ + duration_in_tenths_of_second = int(min(duration_in_ms / 100, 255)) + timeout_in_seconds = (duration_in_tenths_of_second + 1) / 10.0 + data = "FF00400D04{:02X}000101".format(duration_in_tenths_of_second) + self.ccid_xfr_block(bytearray.fromhex(data), + timeout=timeout_in_seconds) + + def send_ack(self): + # Send an ACK frame, usually to terminate most recent command. + self.ccid_xfr_block(Chipset.ACK) + + def ccid_xfr_block(self, data, timeout=0.1): + """Encapsulate host command *data* into an PC/SC Escape command to + send to the device and extract the chip response if received + within *timeout* seconds. + + """ + frame = struct.pack(" +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +# +# Driver for the Arygon contactless reader with USB serial interface +# +from . import pn531 +from . import pn532 + +import os +import time +import errno + +import logging +log = logging.getLogger(__name__) + + +class ChipsetA(pn531.Chipset): + def write_frame(self, frame): + self.transport.write(b"2" + frame) + + +class DeviceA(pn531.Device): + def close(self): + self.chipset.transport.tty.write(b"0au") # device reset + self.chipset.close() + self.chipset = None + + +class ChipsetB(pn532.Chipset): + def write_frame(self, frame): + self.transport.write(b"2" + frame) + + +class DeviceB(pn532.Device): + def close(self): + self.chipset.transport.tty.write(b"0au") # device reset + self.chipset.close() + self.chipset = None + + +def init(transport): + transport.open(transport.port, 115200) + transport.tty.write(b"0av") # read version + response = transport.tty.readline() + if response.startswith(b"FF00000600V"): + log.debug("Arygon Reader AxxB Version %s", + response[11:].strip().decode()) + transport.tty.timeout = 0.5 + transport.tty.write(b"0at05") + if transport.tty.readline().startswith(b"FF0000"): + log.debug("MCU/TAMA communication set to 230400 bps") + transport.tty.write(b"0ah05") + if transport.tty.readline().startswith(b"FF0000"): + log.debug("MCU/HOST communication set to 230400 bps") + transport.tty.baudrate = 230400 + transport.tty.timeout = 0.1 + time.sleep(0.1) + chipset = ChipsetB(transport, logger=log) + device = DeviceB(chipset, logger=log) + device._vendor_name = "Arygon" + device._device_name = "ADRB" + return device + + transport.open(transport.port, 9600) + transport.tty.write(b"0av") # read version + response = transport.tty.readline() + if response.startswith(b"FF00000600V"): + log.debug("Arygon Reader AxxA Version %s", + response[11:].strip().decode()) + transport.tty.timeout = 0.5 + transport.tty.write(b"0at05") + if transport.tty.readline().startswith(b"FF0000"): + log.debug("MCU/TAMA communication set to 230400 bps") + transport.tty.write(b"0ah05") + if transport.tty.readline().startswith(b"FF0000"): + log.debug("MCU/HOST communication set to 230400 bps") + transport.tty.baudrate = 230400 + transport.tty.timeout = 0.1 + time.sleep(0.1) + chipset = ChipsetA(transport, logger=log) + device = DeviceA(chipset, logger=log) + device._vendor_name = "Arygon" + device._device_name = "ADRA" + return device + + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) diff --git a/src/lib/nfc/clf/device.py b/src/lib/nfc/clf/device.py new file mode 100644 index 0000000..69c0622 --- /dev/null +++ b/src/lib/nfc/clf/device.py @@ -0,0 +1,660 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""All contactless drivers must implement the interface defined in +:class:`~nfc.clf.device.Device`. Unsupported target discovery or target +emulation methods raise :exc:`~nfc.clf.UnsupportedTargetError`. The +interface is used internally by :class:`~nfc.clf.ContactlessFrontend` +and is not intended as an application programming interface. Device +driver methods are not thread-safe and do not necessarily check input +arguments when they are supposed to be valid. The interface may change +without notice at any time. + +""" +from . import transport + +import os +import sys +import errno +import importlib + +import logging +log = logging.getLogger(__name__) + +usb_device_map = { + (0x054c, 0x0193): "pn531", # PN531 (Sony VID/PID) + (0x04cc, 0x0531): "pn531", # PN531 (Philips VID/PID), SCM SCL3710 + (0x04cc, 0x2533): "pn533", # NXP PN533 demo board + (0x04e6, 0x5591): "pn533", # SCM SCL3711 + (0x04e6, 0x5593): "pn533", # SCM SCL3712 + (0x054c, 0x02e1): "rcs956", # Sony RC-S330/360/370 + (0x054c, 0x06c1): "rcs380", # Sony RC-S380 + (0x054c, 0x06c3): "rcs380", # Sony RC-S380 + (0x072f, 0x2200): "acr122", # ACS ACR122U +} + +tty_driver_list = ["arygon", "pn532"] + + +def connect(path): + """Connect to a local device identified by *path* and load the + appropriate device driver. The *path* argument is documented at + :meth:`nfc.clf.ContactlessFrontend.open`. The return value is + either a :class:`Device` instance or :const:`None`. Note that not + all drivers can be autodetected, specifically for serial devices + *path* must usually also specify the driver. + + """ + assert isinstance(path, str) and len(path) > 0 + + found = transport.USB.find(path) + if found is not None: + for vid, pid, bus, dev in found: + module = usb_device_map.get((vid, pid)) + if module is None: + continue + + log.debug("loading {mod} driver for usb:{vid:04x}:{pid:04x}" + .format(mod=module, vid=vid, pid=pid)) + + if sys.platform.startswith("linux"): + devnode = "/dev/bus/usb/%03d/%03d" % (int(bus), int(dev)) + if not os.access(devnode, os.R_OK | os.W_OK): + log.debug("access denied to " + devnode) + if len(path.split(':')) < 3: + continue + else: + raise IOError(errno.EACCES, os.strerror(errno.EACCES)) + + driver = importlib.import_module("nfc.clf." + module) + try: + device = driver.init(transport.USB(bus, dev)) + except IOError as error: + log.debug(error) + if len(path.split(':')) < 3: + continue + else: + raise error + + device._path = "usb:{0:03}:{1:03}".format(int(bus), int(dev)) + return device + + found = transport.TTY.find(path) + if found is not None: + devices = found[0] + drivers = [found[1]] if found[1] else tty_driver_list + globbed = found[2] or drivers is tty_driver_list + for drv in drivers: + for dev in devices: + log.debug("trying {0} on {1}".format(drv, dev)) + driver = importlib.import_module("nfc.clf." + drv) + tty = None + try: + tty = transport.TTY(dev) + device = driver.init(tty) + device._path = dev + return device + except IOError as error: + log.debug(error) + if tty is not None: + tty.close() + if not globbed: + raise + + if path.startswith("udp"): + path = path.split(':') + host = str(path[1]) if len(path) > 1 and path[1] else 'localhost' + port = int(path[2]) if len(path) > 2 and path[2] else 54321 + driver = importlib.import_module("nfc.clf.udp") + device = driver.init(host, port) + device._path = "udp:{0}:{1}".format(host, port) + return device + + +class Device(object): + """All device drivers inherit from the :class:`Device` class and must + implement it's methods. + + """ + def __init__(self, *args, **kwargs): + fname = "__init__" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def __str__(self): + strings = (self.vendor_name, self.product_name, self.chipset_name) + return ' '.join(filter(bool, strings)) + " at " + self.path + + @property + def vendor_name(self): + """The device vendor name. An empty string if the vendor name could + not be determined. + + """ + return self._vendor_name if hasattr(self, "_vendor_name") else '' + + @property + def product_name(self): + """The device product name. An empty string if the product name could + not be determined. + + """ + return self._device_name if hasattr(self, "_device_name") else '' + + @property + def chipset_name(self): + """The name of the chipset embedded in the device.""" + return self._chipset_name + + @property + def path(self): + return self._path + + def close(self): + fname = "close" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def mute(self): + """Mutes all existing communication, most notably the device will no + longer generate a 13.56 MHz carrier signal when operating as + Initiator. + + """ + fname = "mute" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def sense_tta(self, target): + """Discover a Type A Target. + + Activates the 13.56 MHz carrier signal and sends a SENS_REQ + command at the bitrate set by **target.brty**. If a response + is received, sends an RID_CMD for a Type 1 Tag or SDD_REQ and + SEL_REQ for a Type 2/4 Tag and returns the responses. + + Arguments: + + target (nfc.clf.RemoteTarget): Supplies bitrate and optional + command data for the target discovery. The only sensible + command to set is **sel_req** populated with a UID to find + only that specific target. + + Returns: + + nfc.clf.RemoteTarget: Response data received from a remote + target if found. This includes at least **sens_res** and + either **rid_res** (for a Type 1 Tag) or **sdd_res** and + **sel_res** (for a Type 2/4 Tag). + + Raises: + + nfc.clf.UnsupportedTargetError: The method is not supported + or the *target* argument requested an unsupported bitrate + (or has a wrong technology type identifier). + + """ + fname = "sense_tta" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def sense_ttb(self, target): + """Discover a Type B Target. + + Activates the 13.56 MHz carrier signal and sends a SENSB_REQ + command at the bitrate set by **target.brty**. If a SENSB_RES + is received, returns a target object with the **sensb_res** + attribute. + + Note that the firmware of some devices (least all those based + on PN53x) automatically sends an ATTRIB command with varying + but always unfortunate communication settings. The drivers + correct that situation by sending S(DESELECT) and WUPB before + return. + + Arguments: + + target (nfc.clf.RemoteTarget): Supplies bitrate and the + optional **sensb_req** for target discovery. Most drivers + do no not allow a fully customized SENSB_REQ, the only + parameter that can always be changed is the AFI byte, + others may be ignored. + + Returns: + + nfc.clf.RemoteTarget: Response data received from a remote + target if found. The only response data attribute is + **sensb_res**. + + Raises: + + nfc.clf.UnsupportedTargetError: The method is not supported + or the *target* argument requested an unsupported bitrate + (or has a wrong technology type identifier). + + """ + fname = "sense_ttb" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def sense_ttf(self, target): + """Discover a Type F Target. + + Activates the 13.56 MHz carrier signal and sends a SENSF_REQ + command at the bitrate set by **target.brty**. If a SENSF_RES + is received, returns a target object with the **sensf_res** + attribute. + + Arguments: + + target (nfc.clf.RemoteTarget): Supplies bitrate and the + optional **sensf_req** for target discovery. The default + SENSF_REQ invites all targets to respond and requests the + system code information bytes. + + Returns: + + nfc.clf.RemoteTarget: Response data received from a remote + target if found. The only response data attribute is + **sensf_res**. + + Raises: + + nfc.clf.UnsupportedTargetError: The method is not supported + or the *target* argument requested an unsupported bitrate + (or has a wrong technology type identifier). + + """ + fname = "sense_ttf" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def sense_dep(self, target): + """Discover a NFC-DEP Target in active communication mode. + + Activates the 13.56 MHz carrier signal and sends an ATR_REQ + command at the bitrate set by **target.brty**. If an ATR_RES + is received, returns a target object with the **atr_res** + attribute. + + Note that some drivers (like pn531) may modify the transport + data bytes length reduction value in ATR_REQ and ATR_RES due + to hardware limitations. + + Arguments: + + target (nfc.clf.RemoteTarget): Supplies bitrate and the + mandatory **atr_req** for target discovery. The bitrate + may be one of '106A', '212F', or '424F'. + + Returns: + + nfc.clf.RemoteTarget: Response data received from a remote + target if found. The only response data attribute is + **atr_res**. The actually sent and potentially modified + ATR_REQ is also included as **atr_req** attribute. + + Raises: + + nfc.clf.UnsupportedTargetError: The method is not supported + or the *target* argument requested an unsupported bitrate + (or has a wrong technology type identifier). + + """ + fname = "sense_dep" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def listen_tta(self, target, timeout): + """Listen as Type A Target. + + Waits to receive a SENS_REQ command at the bitrate set by + **target.brty** and sends the **target.sens_res** + response. Depending on the SENS_RES bytes, the Initiator then + sends an RID_CMD (SENS_RES coded for a Type 1 Tag) or SDD_REQ + and SEL_REQ (SENS_RES coded for a Type 2/4 Tag). Responses are + then generated from the **rid_res** or **sdd_res** and + **sel_res** attributes in *target*. + + Note that none of the currently supported hardware can + actually receive an RID_CMD, thus Type 1 Tag emulation is + impossible. + + Arguments: + + target (nfc.clf.LocalTarget): Supplies bitrate and mandatory + response data to reply when being discovered. + + timeout (float): The maximum number of seconds to wait for a + discovery command. + + Returns: + + nfc.clf.LocalTarget: Command data received from the remote + Initiator if being discovered and to the extent supported + by the device. The first command received after discovery + is returned as one of the **tt1_cmd**, **tt2_cmd** or + **tt4_cmd** attribute (note that unset attributes are + always None). + + Raises: + + nfc.clf.UnsupportedTargetError: The method is not supported + or the *target* argument requested an unsupported bitrate + (or has a wrong technology type identifier). + + ~exceptions.ValueError: A required target response attribute + is not present or does not supply the number of bytes + expected. + + """ + fname = "listen_tta" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def listen_ttb(self, target, timeout): + """Listen as Type A Target. + + Waits to receive a SENSB_REQ command at the bitrate set by + **target.brty** and sends the **target.sensb_res** + response. + + Note that none of the currently supported hardware can + actually listen as Type B target. + + Arguments: + + target (nfc.clf.LocalTarget): Supplies bitrate and mandatory + response data to reply when being discovered. + + timeout (float): The maximum number of seconds to wait for a + discovery command. + + Returns: + + nfc.clf.LocalTarget: Command data received from the remote + Initiator if being discovered and to the extent supported + by the device. The first command received after discovery + is returned as **tt4_cmd** attribute. + + Raises: + + nfc.clf.UnsupportedTargetError: The method is not supported + or the *target* argument requested an unsupported bitrate + (or has a wrong technology type identifier). + + ~exceptions.ValueError: A required target response attribute + is not present or does not supply the number of bytes + expected. + + """ + fname = "listen_ttb" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def listen_ttf(self, target, timeout): + """Listen as Type A Target. + + Waits to receive a SENSF_REQ command at the bitrate set by + **target.brty** and sends the **target.sensf_res** + response. Then waits for a first command that is not a + SENSF_REQ and returns this as the **tt3_cmd** attribute. + + Arguments: + + target (nfc.clf.LocalTarget): Supplies bitrate and mandatory + response data to reply when being discovered. + + timeout (float): The maximum number of seconds to wait for a + discovery command. + + Returns: + + nfc.clf.LocalTarget: Command data received from the remote + Initiator if being discovered and to the extent supported + by the device. The first command received after discovery + is returned as **tt3_cmd** attribute. + + Raises: + + nfc.clf.UnsupportedTargetError: The method is not supported + or the *target* argument requested an unsupported bitrate + (or has a wrong technology type identifier). + + ~exceptions.ValueError: A required target response attribute + is not present or does not supply the number of bytes + expected. + + """ + fname = "listen_ttf" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def listen_dep(self, target, timeout): + """Listen as NFC-DEP Target. + + Waits to receive an ATR_REQ (if the local device supports + active communication mode) or a Type A or F Target activation + followed by an ATR_REQ in passive communication mode. The + ATR_REQ is replied with **target.atr_res**. The first DEP_REQ + command is returned as the **dep_req** attribute along with + **atr_req** and **atr_res**. The **psl_req** and **psl_res** + attributes are returned when the has Initiator performed a + parameter selection. The **sens_res** or **sensf_res** + attributes are returned when activation was in passive + communication mode. + + Arguments: + + target (nfc.clf.LocalTarget): Supplies mandatory response + data to reply when being discovered. All of **sens_res**, + **sdd_res**, **sel_res**, **sensf_res**, and **atr_res** + must be provided. The bitrate does not need to be set, an + NFC-DEP Target always accepts discovery at '106A', '212F + and '424F'. + + timeout (float): The maximum number of seconds to wait for a + discovery command. + + Returns: + + nfc.clf.LocalTarget: Command data received from the remote + Initiator if being discovered and to the extent supported + by the device. The first command received after discovery + is returned as **dep_req** attribute. + + Raises: + + nfc.clf.UnsupportedTargetError: The method is not supported + by the local hardware. + + ~exceptions.ValueError: A required target response attribute + is not present or does not supply the number of bytes + expected. + + """ + fname = "listen_dep" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def send_cmd_recv_rsp(self, target, data, timeout): + """Exchange data with a remote Target + + Sends command *data* to the remote *target* discovered in the + most recent call to one of the sense_xxx() methods. Note that + *target* becomes invalid with any call to mute(), sense_xxx() + or listen_xxx() + + Arguments: + + target (nfc.clf.RemoteTarget): The target returned by the + last successful call of a sense_xxx() method. + + data (bytearray): The binary data to send to the remote + device. + + timeout (float): The maximum number of seconds to wait for + response data from the remote device. + + Returns: + + bytearray: Response data received from the remote device. + + Raises: + + nfc.clf.CommunicationError: When no data was received. + + """ + fname = "send_cmd_recv_rsp" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def send_rsp_recv_cmd(self, target, data, timeout=None): + """Exchange data with a remote Initiator + + Sends response *data* as the local *target* being discovered + in the most recent call to one of the listen_xxx() methods. + Note that *target* becomes invalid with any call to mute(), + sense_xxx() or listen_xxx() + + Arguments: + + target (nfc.clf.LocalTarget): The target returned by the + last successful call of a listen_xxx() method. + + data (bytearray): The binary data to send to the remote + device. + + timeout (float): The maximum number of seconds to wait for + command data from the remote device. + + Returns: + + bytearray: Command data received from the remote device. + + Raises: + + nfc.clf.CommunicationError: When no data was received. + + """ + fname = "send_rsp_recv_cmd" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def get_max_send_data_size(self, target): + """Returns the maximum number of data bytes for sending. + + The maximum number of data bytes acceptable for sending with + either :meth:`send_cmd_recv_rsp` or :meth:`send_rsp_recv_cmd`. + The value reflects the local device capabilities for sending + in the mode determined by *target*. It does not relate to any + protocol capabilities and negotiations. + + Arguments: + + target (nfc.clf.Target): The current local or remote + communication target. + + Returns: + + int: Maximum number of data bytes supported for sending. + + """ + fname = "get_max_send_data_size" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def get_max_recv_data_size(self, target): + """Returns the maximum number of data bytes for receiving. + + The maximum number of data bytes acceptable for receiving with + either :meth:`send_cmd_recv_rsp` or :meth:`send_rsp_recv_cmd`. + The value reflects the local device capabilities for receiving + in the mode determined by *target*. It does not relate to any + protocol capabilities and negotiations. + + Arguments: + + target (nfc.clf.Target): The current local or remote + communication target. + + Returns: + + int: Maximum number of data bytes supported for receiving. + + """ + fname = "get_max_recv_data_size" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def turn_on_led_and_buzzer(self): + """If a device has an LED and/or a buzzer, this method can be + implemented to turn those indicators to the ON state. + + """ + pass + + def turn_off_led_and_buzzer(self): + """If a device has an LED and/or a buzzer, this method can be + implemented to turn those indicators to the OFF state. + + """ + pass + + @staticmethod + def add_crc_a(data): + # Calculate CRC-A for bytearray *data* and return *data* + # extended with the two CRC bytes. + crc = calculate_crc(data, len(data), 0x6363) + return data + bytearray([crc & 0xff, crc >> 8]) + + @staticmethod + def check_crc_a(data): + # Calculate CRC-A for the leading *len(data)-2* bytes of + # bytearray *data* and return whether the result matches the + # trailing 2 bytes of *data*. + crc = calculate_crc(data, len(data)-2, 0x6363) + return (data[-2], data[-1]) == (crc & 0xff, crc >> 8) + + @staticmethod + def add_crc_b(data): + # Calculate CRC-B for bytearray *data* and return *data* + # extended with the two CRC bytes. + crc = ~calculate_crc(data, len(data), 0xFFFF) & 0xFFFF + return data + bytearray([crc & 0xff, crc >> 8]) + + @staticmethod + def check_crc_b(data): + # Calculate CRC-B for the leading *len(data)-2* bytes of + # bytearray *data* and return whether the result matches the + # trailing 2 bytes of *data*. + crc = ~calculate_crc(data, len(data)-2, 0xFFFF) & 0xFFFF + return (data[-2], data[-1]) == (crc & 0xff, crc >> 8) + + +def calculate_crc(data, size, reg): + for octet in data[:size]: + for pos in range(8): + bit = (reg ^ ((octet >> pos) & 1)) & 1 + reg = reg >> 1 + if bit: + reg = reg ^ 0x8408 + return reg diff --git a/src/lib/nfc/clf/pn531.py b/src/lib/nfc/clf/pn531.py new file mode 100644 index 0000000..8153922 --- /dev/null +++ b/src/lib/nfc/clf/pn531.py @@ -0,0 +1,316 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""Driver module for contactless devices based on the NXP PN531 +chipset. This was once a (sort of) joint development between Philips +and Sony to supply hardware capable of running the ISO/IEC 18092 Data +Exchange Protocol. The chip has selectable UART, I2C, SPI, or USB host +interfaces, For USB the vendor and product ID can be switched by a +hardware pin to either Philips or Sony. + +The internal chipset architecture comprises a small 8-bit MCU and a +Contactless Interface Unit CIU that is basically a PN511. The CIU +implements the analog and digital part of communication (modulation +and framing) while the MCU handles the protocol parts and host +communication. The PN511 and hence the PN531 does not support Type B +Technology and can not handle the specific Jewel/Topaz (Type 1 Tag) +communication. Compared to PN532/PN533 the host frame structure does +not allow maximum size ISO/IEC 18092 packets to be transferred. The +driver handles this restriction by modifying the initialization +commands (ATR, PSL) when needed. + +========== ======= ============ +function support remarks +========== ======= ============ +sense_tta yes Type 1 Tag is not supported +sense_ttb no +sense_ttf yes +sense_dep yes Reduced transport data byte length (max 192) +listen_tta yes +listen_ttb no +listen_ttf yes Maximimum frame size is 64 byte +listen_dep yes +========== ======= ============ + +""" +import nfc.clf +from . import pn53x + +import logging +log = logging.getLogger(__name__) + + +class Chipset(pn53x.Chipset): + CMD = { + # Miscellaneous + 0x00: "Diagnose", + 0x02: "GetFirmwareVersion", + 0x04: "GetGeneralStatus", + 0x06: "ReadRegister", + 0x08: "WriteRegister", + 0x0C: "ReadGPIO", + 0x0E: "WriteGPIO", + 0x10: "SetSerialBaudrate", + 0x12: "SetTAMAParameters", + 0x14: "SAMConfiguration", + 0x16: "PowerDown", + # RF communication + 0x32: "RFConfiguration", + 0x58: "RFRegulationTest", + # Initiator + 0x56: "InJumpForDEP", + 0x46: "InJumpForPSL", + 0x4A: "InListPassiveTarget", + 0x50: "InATR", + 0x4E: "InPSL", + 0x40: "InDataExchange", + 0x42: "InCommunicateThru", + 0x44: "InDeselect", + 0x52: "InRelease", + 0x54: "InSelect", + # Target + 0x8C: "TgInitTAMATarget", + 0x92: "TgSetGeneralBytes", + 0x86: "TgGetDEPData", + 0x8E: "TgSetDEPData", + 0x94: "TgSetMetaDEPData", + 0x88: "TgGetInitiatorCommand", + 0x90: "TgResponseToInitiator", + 0x8A: "TgGetTargetStatus", + } + ERR = { + 0x01: "Time out, the Target has not answered", + 0x02: "Checksum error during RF communication", + 0x03: "Parity error during RF communication", + 0x04: "Erroneous bit count in anticollision", + 0x05: "Framing error during Mifare operation", + 0x06: "Abnormal bit collision in 106 kbps anticollision", + 0x07: "Insufficient communication buffer size", + 0x09: "RF buffer overflow detected by CIU", + 0x0a: "RF field not activated in time by active mode peer", + 0x0b: "Protocol error during RF communication", + 0x0d: "Overheated - antenna drivers deactivated", + 0x0e: "Internal buffer overflow", + 0x10: "Invalid command parameter", + 0x12: "Unsupported command from Initiator", + 0x13: "Format error during RF communication", + 0x14: "Mifare authentication error", + 0x23: "ISO/IEC14443-3 UID check byte is wrong", + 0x25: "Command invalid in current DEP state", + 0x26: "Operation not allowed in this configuration", + 0x27: "Command is not acceptable in the current context", + 0x7f: "Invalid command syntax - received error frame", + 0xff: "Insufficient data received from executing chip command", + } + + host_command_frame_max_size = 254 + """Maximum host command frame size.""" + + in_list_passive_target_max_target = 2 + """Maximum number of targets for the InListPassiveTarget command.""" + + in_list_passive_target_brty_range = (0, 1, 2) + """Possible values for the brty parameter to InListPassiveTarget.""" + + def _read_register(self, data): + return self.command(0x06, data, timeout=0.25) + + def _write_register(self, data): + self.command(0x08, data, timeout=0.25) + + sam_configuration_modes = ("normal", "virtual", "wired", "dual") + """Possible SAM configuration modes.""" + + def sam_configuration(self, mode, timeout=0): + """Send the SAMConfiguration command to configure the Security Access + Module. The *mode* argument must be one of the string values + in :data:`sam_configuration_modes`. The *timeout* argument is + only relevant for the virtual card configuration mode. + + """ + mode = self.sam_configuration_modes.index(mode) + 1 + self.command(0x14, bytearray([mode, timeout]), timeout=0.1) + + power_down_wakeup_sources = ("INT0", "INT1", "USB", "RF", "HSU", "SPI") + """Possible wake up sources for the :meth:`power_down` method.""" + + def power_down(self, wakeup_enable): + """Send the PowerDown command to put the PN531 (including the + contactless analog front end) into power down mode in order to + save power consumption. The *wakeup_enable* argument must be a + list of wake up sources with values from the + :data:`power_down_wakeup_sources`. + + """ + wakeup_set = 0 + for i, src in enumerate(self.power_down_wakeup_sources): + if src in wakeup_enable: + wakeup_set |= 1 << i + data = self.command(0x16, bytearray([wakeup_set]), timeout=0.1) + if data[0] != 0: + self.chipset_error(data) + + def tg_init_tama_target(self, mode, mifare_params, felica_params, + nfcid3t, gt, timeout): + """Send the TgInitTAMATarget command.""" + assert type(mode) is int and mode & 0b11111100 == 0 + assert len(mifare_params) == 6 + assert len(felica_params) == 18 + assert len(nfcid3t) == 10 + + data = bytearray([mode]) + mifare_params + felica_params + nfcid3t + gt + return self.command(0x8c, data, timeout) + + +class Device(pn53x.Device): + # Device driver for PN531 based contactless frontends. + + def __init__(self, chipset, logger): + assert isinstance(chipset, Chipset) + super(Device, self).__init__(chipset, logger) + + ver, rev = self.chipset.get_firmware_version() + self._chipset_name = "PN531v{0}.{1}".format(ver, rev) + self.log.debug("chipset is a {0}".format(self._chipset_name)) + + self.chipset.sam_configuration("normal") + self.chipset.set_parameters(0b00000000) + self.chipset.rf_configuration(0x02, b"\x00\x0B\x0A") + self.chipset.rf_configuration(0x04, b"\x00") + self.chipset.rf_configuration(0x05, b"\x01\x00\x01") + self.mute() + + def close(self): + self.mute() + super(Device, self).close() + + def sense_tta(self, target): + """Activate the RF field and probe for a Type A Target. + + The PN531 can discover some Type A Targets (Type 2 Tag and + Type 4A Tag) at 106 kbps. Type 1 Tags (Jewel/Topaz) are + completely unsupported. Because the firmware does not evaluate + the SENS_RES before sending SDD_REQ, it may be that a warning + message about missing Type 1 Tag support is logged even if a + Type 2 or 4A Tag was present. This typically happens when the + SDD_RES or SEL_RES are lost due to communication errors + (normally when the tag is moved away). + + """ + target = super(Device, self).sense_tta(target) + if target and target.sdd_res and len(target.sdd_res) > 4: + # Remove the cascade tag(s) from SDD_RES, only the PN531 + # has them included and we've set the policy that cascade + # tags are not part of the sel_req/sdd_res parameters. + if len(target.sdd_res) == 8: + target.sdd_res = target.sdd_res[1:] + elif len(target.sdd_res) == 12: + target.sdd_res = target.sdd_res[1:4] + target.sdd_res[5:] + # Also the SENS_RES bytes are reversed compared to PN532/533 + target.sens_res = bytearray(reversed(target.sens_res)) + return target + + def sense_ttb(self, target): + """Sense for a Type B Target is not supported.""" + info = "{device} does not support sense for Type B Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def sense_ttf(self, target): + """Activate the RF field and probe for a Type F Target. + + """ + return super(Device, self).sense_ttf(target) + + def sense_dep(self, target): + """Search for a DEP Target in active communication mode. + + Because the PN531 does not implement the extended frame syntax + for host controller communication, it can not support the + maximum payload size of 254 byte. The driver handles this by + modifying the length-reduction values in atr_req and atr_res. + + """ + if target.atr_req[15] & 0x30 == 0x30: + self.log.warning("must reduce the max payload size in atr_req") + target.atr_req[15] = (target.atr_req[15] & 0xCF) | 0x20 + + target = super(Device, self).sense_dep(target) + if target is None: + return + + if target.atr_res[16] & 0x30 == 0x30: + self.log.warning("must reduce the max payload size in atr_res") + atr_res = bytearray(target.atr_res) + atr_res[16] = (target.atr_res[16] & 0xCF) | 0x20 + target.atr_res = bytes(atr_res) + + return target + + def listen_tta(self, target, timeout): + """Listen *timeout* seconds for a Type A activation at 106 kbps. The + ``sens_res``, ``sdd_res``, and ``sel_res`` response data must + be provided and ``sdd_res`` must be a 4 byte UID that starts + with ``08h``. Depending on ``sel_res`` an activation may + return a target with a ``tt2_cmd``, ``tt4_cmd`` or ``atr_req`` + attribute. The default RATS response sent for a Type 4 Tag + activation can be replaced with a ``rats_res`` attribute. + + """ + return super(Device, self).listen_tta(target, timeout) + + def listen_ttb(self, target, timeout): + """Listen as Type B Target is not supported.""" + info = "{device} does not support listen as Type B Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_ttf(self, target, timeout): + """Listen *timeout* seconds for a Type F card activation. The target + ``brty`` must be set to either 212F or 424F and ``sensf_res`` + provide 19 byte response data (response code + 8 byte IDm + 8 + byte PMm + 2 byte system code). Note that the maximum command + an response frame length is 64 bytes only (including the frame + length byte), because the driver must directly program the + contactless interface unit within the PN533. + + """ + return super(Device, self).listen_ttf(target, timeout) + + def listen_dep(self, target, timeout): + """Listen *timeout* seconds to become initialized as a DEP Target. + + The PN531 can be set to listen as a DEP Target for passive and + active communication mode. + + """ + return super(Device, self).listen_dep(target, timeout) + + def _init_as_target(self, mode, tta_params, ttf_params, timeout): + nfcid3t = ttf_params[0:8] + b"\x00\x00" + args = (mode, tta_params, ttf_params, nfcid3t, b'', timeout) + return self.chipset.tg_init_tama_target(*args) + + +def init(transport): + chipset = Chipset(transport, logger=log) + device = Device(chipset, logger=log) + device._vendor_name = transport.manufacturer_name + device._device_name = transport.product_name + return device diff --git a/src/lib/nfc/clf/pn532.py b/src/lib/nfc/clf/pn532.py new file mode 100644 index 0000000..5ef8d91 --- /dev/null +++ b/src/lib/nfc/clf/pn532.py @@ -0,0 +1,454 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""Driver module for contactless devices based on the NXP PN532 +chipset. This successor of the PN531 can additionally handle Type B +Technology (type 4B Tags) and Type 1 Tag communication. It also +supports an extended frame syntax for host communication that allows +larger packets to be transferred. The chip has selectable UART, I2C or +SPI host interfaces. A speciality of the PN532 is that it can manage +two targets (cards) simultanously, although this is not used by +*nfcpy*. + +The internal chipset architecture comprises a small 8-bit MCU and a +Contactless Interface Unit CIU that is basically a PN512. The CIU +implements the analog and digital part of communication (modulation +and framing) while the MCU handles the protocol parts and host +communication. Almost all PN532 firmware limitations (or bugs) can be +avoided by directly programming the CIU. Type F Target mode for card +emulation is completely implemented with the CIU and limited to 64 +byte frame exchanges by the CIU's FIFO size. Type B Target mode is not +possible. + +========== ======= ============ +function support remarks +========== ======= ============ +sense_tta yes +sense_ttb yes +sense_ttf yes +sense_dep yes +listen_tta yes +listen_ttb no +listen_ttf yes Maximimum frame size is 64 byte +listen_dep yes +========== ======= ============ + +""" +import nfc.clf +from . import pn53x + +import os +import sys +import time +import errno + +import logging +log = logging.getLogger(__name__) + + +class Chipset(pn53x.Chipset): + CMD = { + # Miscellaneous + 0x00: "Diagnose", + 0x02: "GetFirmwareVersion", + 0x04: "GetGeneralStatus", + 0x06: "ReadRegister", + 0x08: "WriteRegister", + 0x0C: "ReadGPIO", + 0x0E: "WriteGPIO", + 0x10: "SetSerialBaudrate", + 0x12: "SetParameters", + 0x14: "SAMConfiguration", + 0x16: "PowerDown", + # RF communication + 0x32: "RFConfiguration", + 0x58: "RFRegulationTest", + # Initiator + 0x56: "InJumpForDEP", + 0x46: "InJumpForPSL", + 0x4A: "InListPassiveTarget", + 0x50: "InATR", + 0x4E: "InPSL", + 0x40: "InDataExchange", + 0x42: "InCommunicateThru", + 0x44: "InDeselect", + 0x52: "InRelease", + 0x54: "InSelect", + 0x60: "InAutoPoll", + # Target + 0x8C: "TgInitAsTarget", + 0x92: "TgSetGeneralBytes", + 0x86: "TgGetData", + 0x8E: "TgSetData", + 0x94: "TgSetMetaData", + 0x88: "TgGetInitiatorCommand", + 0x90: "TgResponseToInitiator", + 0x8A: "TgGetTargetStatus", + } + ERR = { + 0x01: "Time out, the Target has not answered", + 0x02: "Checksum error during RF communication", + 0x03: "Parity error during RF communication", + 0x04: "Erroneous bit count in anticollision", + 0x05: "Framing error during Mifare operation", + 0x06: "Abnormal bit collision in 106 kbps anticollision", + 0x07: "Insufficient communication buffer size", + 0x09: "RF buffer overflow detected by CIU", + 0x0a: "RF field not activated in time by active mode peer", + 0x0b: "Protocol error during RF communication", + 0x0d: "Overheated - antenna drivers deactivated", + 0x0e: "Internal buffer overflow", + 0x10: "Invalid command parameter", + 0x12: "Unsupported command from Initiator", + 0x13: "Format error during RF communication", + 0x14: "Mifare authentication error", + 0x23: "ISO/IEC14443-3 UID check byte is wrong", + 0x25: "Command invalid in current DEP state", + 0x26: "Operation not allowed in this configuration", + 0x27: "Command is not acceptable in the current context", + 0x29: "Released by Initiator while operating as Target", + 0x2A: "ISO/IEC14443-3B, the ID of the card does not match", + 0x2B: "ISO/IEC14443-3B, card previously activated has disappeared", + 0x2C: "NFCID3i and NFCID3t mismatch in DEP 212/424 kbps passive", + 0x2D: "An over-current event has been detected", + 0x2E: "NAD missing in DEP frame", + 0x7f: "Invalid command syntax - received error frame", + 0xff: "Insufficient data received from executing chip command", + } + + host_command_frame_max_size = 265 + in_list_passive_target_max_target = 2 + in_list_passive_target_brty_range = (0, 1, 2, 3, 4) + + def _read_register(self, data): + return self.command(0x06, data, timeout=0.25) + + def _write_register(self, data): + self.command(0x08, data, timeout=0.25) + + def set_serial_baudrate(self, baudrate): + br = (9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 1288000) + + self.command(0x10, bytearray([br.index(baudrate)]), timeout=0.1) + self.write_frame(self.ACK) + time.sleep(0.001) + + def sam_configuration(self, mode, timeout=0, irq=False): + mode = ("normal", "virtual", "wired", "dual").index(mode) + 1 + self.command(0x14, bytearray([mode, timeout, int(irq)]), timeout=0.1) + + power_down_wakeup_src = ("INT0", "INT1", "rfu", "RF", + "HSU", "SPI", "GPIO", "I2C") + + def power_down(self, wakeup_enable, generate_irq=False): + wakeup_set = 0 + for i, src in enumerate(self.power_down_wakeup_src): + if src in wakeup_enable: + wakeup_set |= 1 << i + cmd_data = bytearray([wakeup_set, int(generate_irq)]) + data = self.command(0x16, cmd_data, timeout=0.1) + if data[0] != 0: + self.chipset_error(data) + + def tg_init_as_target(self, mode, mifare_params, felica_params, nfcid3t, + general_bytes=b'', historical_bytes=b'', + timeout=None): + assert type(mode) is int and mode & 0b11111000 == 0 + assert len(mifare_params) == 6 + assert len(felica_params) == 18 + assert len(nfcid3t) == 10 + + data = (bytearray([mode]) + mifare_params + felica_params + nfcid3t + + bytearray([len(general_bytes)]) + general_bytes + + bytearray([len(historical_bytes)]) + historical_bytes) + return self.command(0x8c, data, timeout) + + +class Device(pn53x.Device): + # Device driver for PN532 based contactless frontends. + + def __init__(self, chipset, logger): + assert isinstance(chipset, Chipset) + super(Device, self).__init__(chipset, logger) + + ic, ver, rev, support = self.chipset.get_firmware_version() + self._chipset_name = "PN5{0:02x}v{1}.{2}".format(ic, ver, rev) + self.log.debug("chipset is a {0}".format(self._chipset_name)) + + self.chipset.set_parameters(0b00000000) + self.chipset.rf_configuration(0x02, b"\x00\x0B\x0A") + self.chipset.rf_configuration(0x04, b"\x00") + self.chipset.rf_configuration(0x05, b"\x01\x00\x01") + + self.log.debug("write analog settings for Type A 106 kbps") + data = bytearray.fromhex("59 F4 3F 11 4D 85 61 6F 26 62 87") + self.chipset.rf_configuration(0x0A, data) + + self.log.debug("write analog settings for Type F 212/424 kbps") + data = bytearray.fromhex("69 FF 3F 11 41 85 61 6F") + self.chipset.rf_configuration(0x0B, data) + + self.log.debug("write analog settings for Type B 106 kbps") + data = bytearray.fromhex("FF 04 85") + self.chipset.rf_configuration(0x0C, data) + + self.log.debug("write analog settings for 14443-4 212/424/848 kbps") + data = bytearray.fromhex("85 15 8A 85 08 B2 85 01 DA") + self.chipset.rf_configuration(0x0D, data) + + self.mute() + + def close(self): + # Cancel most recent command in case we've been interrupted + # before the response, give the chip 10 ms to think about it. + self.chipset.send_ack() + time.sleep(0.01) + + # When using the high speed uart we must set the baud rate + # back to 115.2 kbps, otherwise we can't talk next time. + if self.chipset.transport.TYPE == "TTY": + self.chipset.set_serial_baudrate(115200) + self.chipset.transport.baudrate = 115200 + + # Set the chip to sleep mode with some wakeup sources. + self.chipset.power_down(wakeup_enable=("I2C", "SPI", "HSU")) + super(Device, self).close() + + def sense_tta(self, target): + """Search for a Type A Target. + + The PN532 can discover all kinds of Type A Targets (Type 1 + Tag, Type 2 Tag, and Type 4A Tag) at 106 kbps. + + """ + return super(Device, self).sense_tta(target) + + def sense_ttb(self, target): + """Search for a Type B Target. + + The PN532 can discover Type B Targets (Type 4B Tag) at 106 + kbps. For a Type 4B Tag the firmware automatically sends an + ATTRIB command that configures the use of DID and 64 byte + maximum frame size. The driver reverts this configuration with + a DESELECT and WUPB command to return the target prepared for + activation (which nfcpy does in the tag activation code). + + """ + return super(Device, self).sense_ttb(target, did=b'\x01') + + def sense_ttf(self, target): + """Search for a Type F Target. + + The PN532 can discover Type F Targets (Type 3 Tag) at 212 and + 424 kbps. The driver uses the default polling command + ``06FFFF0000`` if no ``target.sens_req`` is supplied. + + """ + return super(Device, self).sense_ttf(target) + + def sense_dep(self, target): + """Search for a DEP Target in active communication mode.""" + return super(Device, self).sense_dep(target) + + def _tt1_send_cmd_recv_rsp(self, data, timeout): + # Special handling for Tag Type 1 (Jewel/Topaz) card commands. + + if data[0] in (0x00, 0x01, 0x1A, 0x53, 0x72): + # These commands are implemented by the chipset. + return self.chipset.in_data_exchange(data, timeout)[0] + + if data[0] == 0x10: + # RSEG implementation does not accept any segment other + # than 0. Unfortunately we can not directly issue this + # command to the CIU because the response is 128 byte and + # we're not fast enough to read it from the 64 byte FIFO. + rsp = data[1:2] + for block in range((data[1] >> 4) * 16, (data[1] >> 4) * 16 + 16): + cmd = bytearray([0x02, block]) + data[2:] + rsp += self._tt1_send_cmd_recv_rsp(cmd, timeout)[1:9] + return rsp + + # Remaining commands READ8, WRITE-E8, WRITE-NE8 are not + # implemented by the chipset. Fortunately we can directly + # program the CIU through register read/write. Each TT1 + # command byte must be send as a separate Type A frame, the + # first as a short frame with only 7 data bits and the others + # as normal frames. Reading is also a bit complicated because + # for sending we have to disable the parity generator which + # means that we will also receive the parity bits, thus 9 bits + # received per 8 data bits. And because they are already + # reversed in the FIFO we must swap before parity removal and + # afterwards (maybe this could be optimized a bit) + data = self.add_crc_b(data) + register_write = [] + register_write.append(("CIU_FIFOData", data[0])) # CMD_CODE + register_write.append(("CIU_BitFraming", 0x07)) # 7 bits + register_write.append(("CIU_Command", 0x04)) # Transmit + register_write.append(("CIU_BitFraming", 0x00)) # 8 bits + register_write.append(("CIU_ManualRCV", 0x30)) # ParityDisable + for i in range(1, len(data)): + register_write.append(("CIU_FIFOData", data[i])) # CMD_DATA + register_write.append(("CIU_Command", 0x04)) # Transmit + register_write.append(("CIU_Command", 0x07)) # NoCmdChange + register_write.append(("CIU_Command", 0x08)) # Receive + self.chipset.write_register(*register_write) + if data[0] == 0x54: # WRITE-E8 + time.sleep(0.006) # assuming same response time as WRITE-E + if data[0] == 0x1B: # WRITE-NE8 + time.sleep(0.003) # assuming same response time as WRITE-NE + self.chipset.write_register(("CIU_ManualRCV", 0x20)) # enable parity + fifo_level = self.chipset.read_register("CIU_FIFOLevel") + if fifo_level == 0: + raise nfc.clf.TimeoutError + data = self.chipset.read_register(*(fifo_level * ["CIU_FIFOData"])) + data = ''.join(["{:08b}".format(octet)[::-1] for octet in data]) + data = [int(data[i:i+8][::-1], 2) for i in range(0, len(data)-8, 9)] + if self.check_crc_b(data) is False: + raise nfc.clf.TransmissionError("crc_b check error") + return bytearray(data[0:-2]) + + def listen_tta(self, target, timeout): + """Listen *timeout* seconds for a Type A activation at 106 kbps. The + ``sens_res``, ``sdd_res``, and ``sel_res`` response data must + be provided and ``sdd_res`` must be a 4 byte UID that starts + with ``08h``. Depending on ``sel_res`` an activation may + return a target with a ``tt2_cmd``, ``tt4_cmd`` or ``atr_req`` + attribute. The default RATS response sent for a Type 4 Tag + activation can be replaced with a ``rats_res`` attribute. + + """ + return super(Device, self).listen_tta(target, timeout) + + def listen_ttb(self, target, timeout): + """Listen as Type B Target is not supported.""" + info = "{device} does not support listen as Type B Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_ttf(self, target, timeout): + """Listen *timeout* seconds for a Type F card activation. The target + ``brty`` must be set to either 212F or 424F and ``sensf_res`` + provide 19 byte response data (response code + 8 byte IDm + 8 + byte PMm + 2 byte system code). Note that the maximum command + an response frame length is 64 bytes only (including the frame + length byte), because the driver must directly program the + contactless interface unit within the PN533. + + """ + return super(Device, self).listen_ttf(target, timeout) + + def listen_dep(self, target, timeout): + """Listen *timeout* seconds to become initialized as a DEP Target. + + The PN532 can be set to listen as a DEP Target for passive and + active communication mode. + + """ + return super(Device, self).listen_dep(target, timeout) + + def _init_as_target(self, mode, tta_params, ttf_params, timeout): + nfcid3t = ttf_params[0:8] + b"\x00\x00" + args = (mode, tta_params, ttf_params, nfcid3t, b'', b'', timeout) + return self.chipset.tg_init_as_target(*args) + + +def init(transport): + if transport.TYPE == "TTY": + baudrate = 115200 # PN532 initial baudrate + transport.open(transport.port, baudrate) + long_preamble = bytearray(10) + + # The PN532 chip should send an ack within 15 ms after a + # command. We'll give it a bit more and wait 100 ms, unless + # we're on a Raspberry Pi detected by the Broadcom SOC. The + # USB on BCM270x has a nasty bug (may be SW or HW) that + # introduces additional up to ~1000 ms delay for the first + # data from a ttyUSB. Tested with two serial converters + # (PL2303 and FT232R) in loopback and it's reproducable adding + # up to 1000 ms if a serial open is done 1 sec after serial + # close. Waiting longer decreases that time until after 2 sec + # wait between close and open it all goes fine until the wait + # time reaches 3 seconds, and so on. + initial_timeout = 100 # milliseconds + # change_baudrate = True # try higher speeds + change_baudrate = False # MOD GG *DO NOT* try higher speeds + if sys.platform.startswith('linux'): + board = b"" # Raspi board will identify through device tree + try: + board = open('/proc/device-tree/model', "rb").read().strip( + b'\x00') + except IOError: + pass + if board.startswith(b"Raspberry Pi"): + log.debug("running on {}".format(board)) + if transport.port.startswith("/dev/ttyUSB"): + log.debug("ttyUSB requires more time for first ack") + initial_timeout = 1500 # milliseconds + elif transport.port == "/dev/ttyS0": + log.debug("ttyS0 can only do 115.2 kbps") + change_baudrate = False # RPi 'mini uart' + + get_version_cmd = bytearray.fromhex("0000ff02fed4022a00") + get_version_rsp = bytearray.fromhex("0000ff06fad50332") + transport.write(long_preamble + get_version_cmd) + log.debug("wait %d ms for data on %s", initial_timeout, transport.port) + if not transport.read(timeout=initial_timeout) == Chipset.ACK: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + if not transport.read(timeout=100).startswith(get_version_rsp): + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + sam_configuration_cmd = bytearray.fromhex("0000ff05fbd4140100001700") + sam_configuration_rsp = bytearray.fromhex("0000ff02fed5151600") + transport.write(long_preamble + sam_configuration_cmd) + if not transport.read(timeout=100) == Chipset.ACK: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + if not transport.read(timeout=100) == sam_configuration_rsp: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + if sys.platform.startswith("linux") and change_baudrate is True: + stty = 'stty -F %s %%d 2> /dev/null' % transport.port + # MOD GG FIXED BAUD RATE + # for baudrate in (921600, 460800, 230400, 115200): + for baudrate in (115200,): + log.debug("trying to set %d baud", baudrate) + if os.system(stty % baudrate) == 0: + os.system(stty % 115200) + break + + if baudrate > 115200: + set_baudrate_cmd = bytearray.fromhex("0000ff03fdd410000000") + set_baudrate_rsp = bytearray.fromhex("0000ff02fed5111a00") + set_baudrate_cmd[7] = 5 + (230400, 460800, 921600).index(baudrate) + set_baudrate_cmd[8] = 256 - sum(set_baudrate_cmd[5:8]) + transport.write(long_preamble + set_baudrate_cmd) + if not transport.read(timeout=100) == Chipset.ACK: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + if not transport.read(timeout=100) == set_baudrate_rsp: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + transport.write(Chipset.ACK) + transport.open(transport.port, baudrate) + log.debug("changed uart speed to %d baud", baudrate) + time.sleep(0.001) + + chipset = Chipset(transport, logger=log) + return Device(chipset, logger=log) + + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) diff --git a/src/lib/nfc/clf/pn533.py b/src/lib/nfc/clf/pn533.py new file mode 100644 index 0000000..d17fc4f --- /dev/null +++ b/src/lib/nfc/clf/pn533.py @@ -0,0 +1,399 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""Driver module for contactless devices based on the NXP PN533 +chipset. The PN533 is pretty similar to the PN532 except that it also +has a USB host interface option and, probably due to the resources +needed for USB, does not support two simultaneous targets. Anything +else said about PN532 also applies to PN533. + +========== ======= ============ +function support remarks +========== ======= ============ +sense_tta yes +sense_ttb yes +sense_ttf yes +sense_dep yes +listen_tta yes +listen_ttb no +listen_ttf yes Maximimum frame size is 64 byte +listen_dep yes +========== ======= ============ + +""" +import nfc.clf +from . import pn53x + +import time + +import logging +log = logging.getLogger(__name__) + + +class Chipset(pn53x.Chipset): + CMD = { + # Miscellaneous + 0x00: "Diagnose", + 0x02: "GetFirmwareVersion", + 0x04: "GetGeneralStatus", + 0x06: "ReadRegister", + 0x08: "WriteRegister", + 0x0C: "ReadGPIO", + 0x0E: "WriteGPIO", + 0x12: "SetParameters", + 0x18: "AlparCommandForTDA", + # RF Communication + 0x32: "RFConfiguration", + 0x58: "RFRegulationTest", + # Initiator + 0x56: "InJumpForDEP", + 0x46: "InJumpForPSL", + 0x4A: "InListPassiveTarget", + 0x50: "InATR", + 0x4E: "InPSL", + 0x40: "InDataExchange", + 0x42: "InCommunicateThru", + 0x38: "InQuartetByteExchange", + 0x44: "InDeselect", + 0x52: "InRelease", + 0x54: "InSelect", + 0x48: "InActivateDeactivatePaypass", + # Target + 0x8C: "TgInitAsTarget", + 0x92: "TgSetGeneralBytes", + 0x86: "TgGetData", + 0x8E: "TgSetData", + 0x96: "TgSetDataSecure", + 0x94: "TgSetMetaData", + 0x98: "TgSetMetaDataSecure", + 0x88: "TgGetInitiatorCommand", + 0x90: "TgResponseToInitiator", + 0x8A: "TgGetTargetStatus", + } + ERR = { + 0x01: "Time out, the Target has not answered", + 0x02: "Checksum error during RF communication", + 0x03: "Parity error during RF communication", + 0x04: "Erroneous bit count in anticollision", + 0x05: "Framing error during mifare operation", + 0x06: "Abnormal bit collision in 106 kbps anticollision", + 0x07: "Insufficient communication buffer size", + 0x09: "RF buffer overflow detected by CIU", + 0x0a: "RF field not activated in time by active mode peer", + 0x0b: "Protocol error during RF communication", + 0x0d: "Overheated - antenna drivers deactivated", + 0x0e: "Internal buffer overflow", + 0x10: "Invalid command parameter", + 0x12: "Unsupported command from Initiator", + 0x13: "Format error during RF communication", + 0x14: "Mifare authentication error", + 0x18: "Target or Initiator does not support NFC Secure", + 0x19: "I2C bus line is busy, a TDA transaction is ongoing", + 0x23: "ISO/IEC14443-3 UID check byte is wrong", + 0x25: "Command invalid in current DEP state", + 0x26: "Operation not allowed in this configuration", + 0x27: "Command is not acceptable due to the current context", + 0x29: "Released by Initiator while operating as Target", + 0x2A: "ISO/IEC14443-3B, the ID of the card does not match", + 0x2B: "ISO/IEC14443-3B, card previously activated has disappeared", + 0x2C: "NFCID3i and NFCID3t mismatch in DEP 212/424 kbps passive", + 0x2D: "An over-current event has been detected", + 0x2E: "NAD missing in DEP frame", + 0x7f: "Invalid command syntax - received error frame", + 0xff: "Insufficient data received from executing chip command", + } + + host_command_frame_max_size = 265 + in_list_passive_target_max_target = 1 + in_list_passive_target_brty_range = (0, 1, 2, 3, 4, 6, 7, 8) + + def get_general_status(self): + data = super(Chipset, self).get_general_status() + err = self.ERR.get(data[0], "error code 0x%02X" % data[0]) + field = ("", "external field detected")[data[1]] + if data[2] == 1: + br_rx = (106, 212, 424, 848)[data[4]] + br_tx = (106, 212, 424, 848)[data[5]] + mtype = {0: "A/B", 1: "Active", 2: "Jewel", 16: "FeliCa"}[data[6]] + return err, field, (data[3], br_rx, br_tx, mtype) + else: + return err, field, None + + def _read_register(self, data): + data = self.command(0x06, data, timeout=0.25) + if data[0] != 0: + self.chipset_error(data) + return data[1:] + + def _write_register(self, data): + data = self.command(0x08, data, timeout=0.25) + if data[0] != 0: + self.chipset_error(data) + + def tg_init_as_target(self, mode, mifare_params, felica_params, + nfcid3t, gt, tk, timeout): + assert type(mode) is int and mode & 0b11111100 == 0 + assert len(mifare_params) == 6 + assert len(felica_params) == 18 + assert len(nfcid3t) == 10 + + data = (bytearray([mode]) + mifare_params + felica_params + nfcid3t + + bytearray([len(gt)]) + gt + bytearray([len(tk)]) + tk) + return self.command(0x8c, data, timeout) + + +class Device(pn53x.Device): + # Device driver for PN533 based contactless frontends. + + def __init__(self, chipset, logger): + assert isinstance(chipset, Chipset) + super(Device, self).__init__(chipset, logger) + + ic, ver, rev, support = self.chipset.get_firmware_version() + self._chipset_name = "PN5{0:02x}v{1}.{2}".format(ic, ver, rev) + self.log.debug("chipset is a {0}".format(self._chipset_name)) + + self.mute() + self.chipset.rf_configuration(0x02, b"\x00\x0B\x0A") + self.chipset.rf_configuration(0x04, b"\x00") + self.chipset.rf_configuration(0x05, b"\x01\x00\x01") + self.chipset.set_parameters(0b00000000) + + self.eeprom = bytearray() + try: + self.chipset.read_register(0xA000) # check access + for addr in range(0xA000, 0xA100, 64): + data = self.chipset.read_register(*range(addr, addr+64)) + self.eeprom.extend(data) + except Chipset.Error: + self.log.debug("no eeprom attached") + + if self.eeprom: + head = "EEPROM " + ' '.join(["%2X" % i for i in range(16)]) + self.log.debug(head) + for i in range(0, len(self.eeprom), 16): + data = ' '.join(["%02X" % x for x in self.eeprom[i:i+16]]) + self.log.debug(('0x%04X: %s' % (0xA000+i, data))) + else: + self.log.debug("no eeprom attached") + + self.log.debug("write analog settings for Type A 106 kbps") + data = bytearray.fromhex("5A F4 3F 11 4D 85 61 6F 26 62 87") + self.chipset.rf_configuration(0x0A, data) + + self.log.debug("write analog settings for Type F 212/424 kbps") + data = bytearray.fromhex("6A FF 3F 10 41 85 61 6F") + self.chipset.rf_configuration(0x0B, data) + + self.log.debug("write analog settings for Type B 106 kbps") + data = bytearray.fromhex("FF 04 85") + self.chipset.rf_configuration(0x0C, data) + + self.log.debug("write analog settings for 14443-4 212/424/848 kbps") + data = bytearray.fromhex("85 15 8A 85 0A B2 85 04 DA") + self.chipset.rf_configuration(0x0D, data) + + def close(self): + self.mute() + super(Device, self).close() + + def sense_tta(self, target): + """Activate the RF field and probe for a Type A Target. + + The PN533 can discover all kinds of Type A Targets (Type 1 + Tag, Type 2 Tag, and Type 4A Tag) at 106 kbps. + + """ + return super(Device, self).sense_tta(target) + + def sense_ttb(self, target): + """Activate the RF field and probe for a Type B Target. + + The PN533 can discover Type B Targets (Type 4B Tag) at 106, + 212, 424, and 848 kbps. The PN533 automatically sends an + ATTRIB command that configures a 64 byte maximum frame + size. The driver reverts this configuration with a DESELECT + and WUPB command to return the target prepared for activation. + + """ + return super(Device, self).sense_ttb(target) + + def sense_ttf(self, target): + """Activate the RF field and probe for a Type F Target. + + The PN533 can discover Type F Targets (Type 3 Tag) at 212 and + 424 kbps. + + """ + return super(Device, self).sense_ttf(target) + + def sense_dep(self, target): + """Search for a DEP Target in active communication mode.""" + return super(Device, self).sense_dep(target) + + def send_cmd_recv_rsp(self, target, data, timeout): + """Send command *data* to the remote *target* and return the response + data if received within *timeout* seconds. + + """ + return super(Device, self).send_cmd_recv_rsp(target, data, timeout) + + def _tt1_send_cmd_recv_rsp(self, data, timeout): + # Special handling for Tag Type 1 (Jewel/Topaz) card commands. + + if data[0] in (0x00, 0x01, 0x1A, 0x53, 0x72): + # RALL, READ, WRITE-NE, WRITE-E, RID are properly + # implemented by the PN533 firmware. + return self.chipset.in_data_exchange(data, timeout)[0] + + if data[0] == 0x10: + # RSEG implementation does not accept any segment other + # than 0. Unfortunately we can not directly issue this + # command to the CIU because the response is 128 byte and + # we're not fast enough to read it from the 64 byte FIFO. + rsp = data[1:2] + for block in range((data[1] >> 4) * 16, (data[1] >> 4) * 16 + 16): + cmd = bytearray([0x02, block]) + data[2:] + rsp += self._tt1_send_cmd_recv_rsp(cmd, timeout)[1:9] + return rsp + + # Remaining commands READ8, WRITE-E8, WRITE-NE8 are not + # implemented by the chipset. Fortunately we can directly + # program the CIU through register read/write. Each TT1 + # command byte must be send as a separate Type A frame, the + # first is a short frame with only 7 data bits and the others + # are normal frames. Reading is also a bit complicated because + # for sending we have to disable the parity generator which + # means that we will also receive the parity bits, thus 9 bits + # received per 8 data bits. And because they are already + # reversed in the FIFO we must swap before parity removal and + # afterwards (maybe this could be a bit more optimized). + data = self.add_crc_b(data) + self.chipset.write_register( + ("CIU_FIFOData", data[0]), # CMD_CODE + ("CIU_ManualRCV", 0x10), # ParityDisable + ("CIU_BitFraming", 0x07), # 7 bits + ("CIU_Command", 0x04), # Transmit + ) + for i in range(1, len(data)-1): + self.chipset.write_register( + ("CIU_FIFOData", data[i]), # CMD_DATA + ("CIU_BitFraming", 0x00), # 8 bits + ("CIU_Command", 0x04), # Transmit + ) + self.chipset.write_register( + ("CIU_FIFOData", data[-1]), # CMD_DATA + ("CIU_Command", 0x0C), # Transceive + ("CIU_BitFraming", 0x80), # 8 bits, start send + ) + if data[0] == 0x54: # WRITE-E8 + time.sleep(0.006) # assuming same response time as WRITE-E + if data[0] == 0x1B: # WRITE-NE8 + time.sleep(0.003) # assuming same response time as WRITE-NE + self.chipset.write_register(("CIU_ManualRCV", 0x00)) # enable parity + fifo_level = self.chipset.read_register("CIU_FIFOLevel") + if fifo_level == 0: + raise nfc.clf.TimeoutError + data = self.chipset.read_register(*(fifo_level * ["CIU_FIFOData"])) + data = ''.join(["{:08b}".format(octet)[::-1] for octet in data]) + data = [int(data[i:i+8][::-1], 2) for i in range(0, len(data)-8, 9)] + if self.check_crc_b(data) is False: + raise nfc.clf.TransmissionError("crc_b check error") + return bytearray(data[:-2]) + + def listen_tta(self, target, timeout): + """Listen *timeout* seconds for a Type A activation at 106 kbps. The + ``sens_res``, ``sdd_res``, and ``sel_res`` response data must + be provided and ``sdd_res`` must be a 4 byte UID that starts + with ``08h``. Depending on ``sel_res`` an activation may + return a target with a ``tt2_cmd``, ``tt4_cmd`` or ``atr_req`` + attribute. The default RATS response sent for a Type 4 Tag + activation can be replaced with a ``rats_res`` attribute. + + """ + return super(Device, self).listen_tta(target, timeout) + + def listen_ttb(self, target, timeout): + """Listen as Type B Target is not supported.""" + info = "{device} does not support listen as Type B Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_ttf(self, target, timeout): + """Listen *timeout* seconds for a Type F card activation. The target + ``brty`` must be set to either 212F or 424F and ``sensf_res`` + provide 19 byte response data (response code + 8 byte IDm + 8 + byte PMm + 2 byte system code). Note that the maximum command + an response frame length is 64 bytes only (including the frame + length byte), because the driver must directly program the + contactless interface unit within the PN533. + + """ + return super(Device, self).listen_ttf(target, timeout) + + def listen_dep(self, target, timeout): + """Listen *timeout* seconds to become initialized as a DEP Target. + + The PN533 can be set to listen as a DEP Target for passive and + active communication mode. + + """ + return super(Device, self).listen_dep(target, timeout) + + def send_rsp_recv_cmd(self, target, data, timeout): + """While operating as *target* send response *data* to the remote + device and return new command data if received within + *timeout* seconds. + + """ + return super(Device, self).send_rsp_recv_cmd(target, data, timeout) + + def _init_as_target(self, mode, tta_params, ttf_params, timeout): + nfcid3t = ttf_params[0:8] + b"\x00\x00" + args = (mode, tta_params, ttf_params, nfcid3t, b'', b'', timeout) + return self.chipset.tg_init_as_target(*args) + + +def init(transport): + # write ack to perform a soft reset, raises IOError(EACCES) if + # someone else has already claimed the USB device. + transport.write(Chipset.ACK) + + chipset = Chipset(transport, logger=log) + device = Device(chipset, logger=log) + + # PN533 bug: Manufacturer and product strings are no longer + # accessible from USB device description after first use with + # slightly larger command frames. Better read it from EEPROM. + if device.eeprom: + index = 0 + while index < len(device.eeprom) and device.eeprom[index] != 0xFF: + tlv_tag, tlv_len = device.eeprom[index], device.eeprom[index+1] + tlv_data = device.eeprom[index+2:index+2+tlv_len] + if tlv_tag == 3: + device._device_name = tlv_data[2:].decode("utf-16-le") + if tlv_tag == 4: + device._vendor_name = tlv_data[2:].decode("utf-16-le") + index += 2 + tlv_len + else: + device._vendor_name = "SensorID" + device._device_name = "StickID" + + return device diff --git a/src/lib/nfc/clf/pn53x.py b/src/lib/nfc/clf/pn53x.py new file mode 100644 index 0000000..6ef9fa0 --- /dev/null +++ b/src/lib/nfc/clf/pn53x.py @@ -0,0 +1,1064 @@ +# -*- coding: latin-1 -*- + +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""This is not really a device driver but a base module that +implements common functionality for the PN53x family of contactless +interface chips, namely the NXP PN531, PN532, PN533 and the Sony +RC-S956. + +""" +import nfc.clf +from . import device + +import os +import time +import errno +from binascii import hexlify +from struct import pack, unpack + +import logging +log = logging.getLogger(__name__) + + +class Chipset(object): + SOF = bytearray.fromhex('0000FF') + ACK = bytearray.fromhex('0000FF00FF00') + REG = { + 0x6331: "CIU_Command", + 0x6332: "CIU_CommIEn", + 0x6333: "CIU_DivIEn", + 0x6334: "CIU_CommIRq", + 0x6335: "CIU_DivIRq", + 0x6336: "CIU_Error", + 0x6337: "CIU_Status1", + 0x6338: "CIU_Status2", + 0x6339: "CIU_FIFOData", + 0x633A: "CIU_FIFOLevel", + 0x633B: "CIU_WaterLevel", + 0x633C: "CIU_Control", + 0x633D: "CIU_BitFraming", + 0x633E: "CIU_Coll", + 0x6301: "CIU_Mode", + 0x6302: "CIU_TxMode", + 0x6303: "CIU_RxMode", + 0x6304: "CIU_TxControl", + 0x6305: "CIU_TxAuto", + 0x6306: "CIU_TxSel", + 0x6307: "CIU_RxSel", + 0x6308: "CIU_RxThreshold", + 0x6309: "CIU_Demod", + 0x630A: "CIU_FelNFC1", + 0x630B: "CIU_FelNFC2", + 0x630C: "CIU_MifNFC", + 0x630D: "CIU_ManualRCV", + 0x630E: "CIU_TypeB", + 0x630F: "CIU_SerialSpeed", + 0x6311: "CIU_CRCResultMSB", + 0x6312: "CIU_CRCResultLSB", + 0x6313: "CIU_GsNOff", + 0x6314: "CIU_ModWidth", + 0x6315: "CIU_TxBitPhase", + 0x6316: "CIU_RFCfg", + 0x6317: "CIU_GsNOn", + 0x6318: "CIU_CWGsP", + 0x6319: "CIU_ModGsP", + 0x631A: "CIU_TMode", + 0x631B: "CIU_TPrescaler", + 0x631C: "CIU_TReloadHi", + 0x631D: "CIU_TReloadLo", + 0x631E: "CIU_TCounterHi", + 0x631F: "CIU_TCounterLo", + 0x6321: "CIU_TestSel1", + 0x6322: "CIU_TestSel2", + 0x6323: "CIU_TestPinEn", + 0x6324: "CIU_TestPinValue", + 0x6325: "CIU_TestBus", + 0x6326: "CIU_AutoTest", + 0x6327: "CIU_Version", + 0x6328: "CIU_AnalogTest", + 0x6329: "CIU_TestDAC1", + 0x632A: "CIU_TestDAC2", + 0x632B: "CIU_TestADC", + 0x632C: "CIU_RFT1", + 0x632D: "CIU_RFT2", + 0x632E: "CIU_RFT3", + 0x632F: "CIU_RFT4", + } + REGBYNAME = {v: k for k, v in REG.items()} + + class Error(Exception): + def __init__(self, errno, strerr): + self.errno, self.strerr = errno, strerr + + def __str__(self): + return "Error 0x{0:02X}: {1}".format(self.errno, self.strerr) + + def chipset_error(self, cause): + if cause is None: + errno = 0xff + elif type(cause) is int: + errno = cause + else: + errno = cause[0] + + strerr = self.ERR.get(errno, "Unknown error code") + raise Chipset.Error(errno, strerr) + + def __init__(self, transport, logger): + self.transport = transport + self.log = logger + + def close(self): + self.transport.close() + self.transport = None + + def command(self, cmd_code, cmd_data, timeout): + """Send a host command and return the chip response. The chip command + is selected by the 8-bit integer *cmd_code*. The command + parameters, if any, are supplied with *cmd_data* as a + bytearray or byte string. The fully constructed command frame + is sent with :meth:`write_frame` and the chip acknowledgement + and response is received with :meth:`read_frame`, those + methods are used by some drivers for additional framing. The + implementation waits 100 ms for the command acknowledgement + and then polls every 100 ms for a response frame until + *timeout* seconds have elapsed. If the response frame is + correct and the response code matches *cmd_code* the data + bytes that follow the response code are returned as a + bytearray (without the trailing checksum and postamble). + + **Exceptions** + + * :exc:`~exceptions.IOError` :const:`errno.ETIMEDOUT` if no + response frame was received before *timeout* seconds. + + * :exc:`~exceptions.IOError` :const:`errno.EIO` if response + frame errors were detected. + + * :exc:`Chipset.Error` if an error response frame or status + error was received. + + """ + if cmd_data is not None: + assert len(cmd_data) <= self.host_command_frame_max_size - 2 + self.log.log(logging.DEBUG-1, "{} {} {:.3f}".format( + self.CMD[cmd_code], hexlify(cmd_data).decode(), timeout)) + + if len(cmd_data) < 254: + head = self.SOF + bytearray([len(cmd_data)+2]) \ + + bytearray([254-len(cmd_data)]) + else: + head = self.SOF + b'\xFF\xFF' + pack(">H", len(cmd_data)+2) + head.append((256 - sum(head[-2:])) & 0xFF) + + data = bytearray([0xD4, cmd_code]) + cmd_data + tail = bytearray([(256 - sum(data)) & 0xFF, 0]) + + try: + self.write_frame(head + data + tail) + frame = self.read_frame(timeout=100) + except IOError: + self.log.error("input/output error while waiting for ack") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + if not frame.startswith(self.SOF): + self.log.error("invalid frame start sequence") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + if frame[0:len(self.ACK)] != self.ACK: + self.log.warning("missing ack frame") + else: + frame = self.ACK + + if timeout is not None and timeout <= 0: + return + + while frame == self.ACK: + try: + frame = self.read_frame(int(1000 * timeout)) + except IOError as error: + if error.errno == errno.ETIMEDOUT: + self.write_frame(self.ACK) # cancel command + time.sleep(0.001) + raise error + + if frame.startswith(self.SOF + b'\xFF\xFF'): + # extended frame + if sum(frame[5:8]) & 0xFF != 0: + self.log.error("frame lenght checksum error") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + if unpack(">H", memoryview(frame[5:7]))[0] != len(frame) - 10: + self.log.error("frame lenght value mismatch") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + del frame[0:8] + elif frame.startswith(self.SOF): + # normal frame + if sum(frame[3:5]) & 0xFF != 0: + self.log.error("frame lenght checksum error") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + if frame[3] != len(frame) - 7: + self.log.error("frame lenght value mismatch") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + del frame[0:5] + else: + self.log.debug("invalid frame start sequence") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + if not sum(frame) & 0xFF == 0: + self.log.error("frame data checksum error") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + if frame[0] == 0x7F: # error frame + self.chipset_error(0x7F) + + if not frame[0] == 0xD5: + self.log.error("invalid frame identifier") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + if not frame[1] == cmd_code + 1: + self.log.error("unexpected response code") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + return frame[2:-2] + + def write_frame(self, frame): + """Write a command *frame* to the chipset.""" + self.transport.write(frame) + + def read_frame(self, timeout): + """Wait *timeout* milliseconds to return a chip response frame.""" + return self.transport.read(timeout) + + def send_ack(self): + # Send an ACK frame, usually to terminate most recent command. + self.transport.write(Chipset.ACK) + + def diagnose(self, test, test_data=None): + """Send a Diagnose command. The *test* argument selects the diagnose + function either by number or the string ``line``, ``rom``, or + ``ram``. For a ``line`` test the implementation sends the + longest possible command frame and verifies that the response + data is identical. For a ``ram`` or ``rom`` test the + implementation verfies the response status. For a *test* + number the implementation appends the byte string *test_data* + and returns the response data bytes. + + """ + if test == "line": + size = self.host_command_frame_max_size - 3 + data = b'\x00' + bytearray([x & 0xFF for x in range(size)]) + return self.command(0x00, data, timeout=1.0) == data + if test == "rom": + data = self.command(0x00, b'\x01', timeout=1.0) + return data and data[0] == 0 + if test == "ram": + data = self.command(0x00, b'\x02', timeout=1.0) + return data and data[0] == 0 + return self.command(0x00, pack('B', test) + test_data, timeout=1.0) + + def get_firmware_version(self): + """Send a GetFirmwareVersion command and return the response data + bytes. + + """ + return self.command(0x02, b'', timeout=0.1) + + def get_general_status(self): + """Send a GetGeneralStatus command and return the response data + bytes. + + """ + data = self.command(0x04, b'', timeout=0.1) + if data is None or len(data) < 3: + raise self.chipset_error(None) + return data + + def read_register(self, *args): + """Send a ReadRegister command for the positional register address or + name arguments. The register values are returned as a list for + multiple arguments or an integer for a single argument. :: + + tx_mode = Chipset.read_register(0x6302) + rx_mode = Chipset.read_register("CIU_RxMode") + tx_mode, rx_mode = Chipset.read_register("CIU_TxMode", "CIU_RxMode") + + """ + def addr(r): + return self.REGBYNAME[r] if type(r) is str else r + + args = [addr(reg) for reg in args] + data = b''.join([pack(">H", reg) for reg in args]) + data = self._read_register(data) + return list(data) if len(data) > 1 else data[0] + + def _read_register(self, data): + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError(cname + "._read_register") + + def write_register(self, *args): + """Send a WriteRegister command. Each positional argument must be an + (address, value) tuple except if exactly two arguments are + supplied as register address and value. A register can also be + selected by name. There is no return value. :: + + Chipset.write_register(0x6301, 0x00) + Chipset.write_register("CIU_Mode", 0x00) + Chipset.write_register((0x6301, 0x00), ("CIU_TxMode", 0x00)) + + """ + def addr(r): + return self.REGBYNAME[r] if type(r) is str else r + + assert type(args) in (tuple, list) + if len(args) == 2 and type(args[1]) == int: + args = [args] + args = [(addr(reg), val) for reg, val in args] + data = b''.join([pack(">HB", reg, val) for reg, val in args]) + self._write_register(data) + + def _write_register(self, data): + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError(cname + "._write_register") + + def set_parameters(self, flags): + """Send a SetParameters command with the 8-bit *flags* integer.""" + self.command(0x12, bytearray([flags]), timeout=0.1) + + def rf_configuration(self, cfg_item, cfg_data): + """Send an RFConfiguration command.""" + self.command(0x32, bytearray([cfg_item]) + bytearray(cfg_data), + timeout=0.1) + + def in_jump_for_dep(self, act_pass, br, passive_data, nfcid3, gi): + """Send an InJumpForDEP command. + + """ + assert act_pass in (False, True) + assert br in (106, 212, 424) + assert len(passive_data) in (0, 4, 5) + assert len(nfcid3) in (0, 10) + assert len(gi) <= 48 + cm = int(bool(act_pass)) + br = (106, 212, 424).index(br) + nf = (bool(passive_data) | bool(nfcid3) << 1 | bool(gi) << 2) + data = bytearray([cm, br, nf]) + passive_data + nfcid3 + gi + data = self.command(0x56, bytearray(data), timeout=3.0) + if data is None or data[0] != 0: + self.chipset_error(data) + return data[2:] + + def in_jump_for_psl(self, act_pass, br, passive_data, nfcid3, gi): + """Send an InJumpForPSL command. + + """ + assert act_pass in (False, True) + assert br in (106, 212, 424) + assert len(passive_data) in (0, 4, 5) + assert len(nfcid3) in (0, 10) + assert len(gi) <= 48 + cm = int(bool(act_pass)) + br = (106, 212, 424).index(br) + nf = (bool(passive_data) | bool(nfcid3) << 1 | bool(gi) << 2) + data = bytearray([cm, br, nf]) + passive_data + nfcid3 + gi + data = self.command(0x46, data, timeout=3.0) + if data is None or data[0] != 0: + self.chipset_error(data) + return data[2:] + + def in_list_passive_target(self, max_tg, brty, initiator_data): + assert max_tg <= self.in_list_passive_target_max_target + assert brty in self.in_list_passive_target_brty_range + data = bytearray([1, brty]) + initiator_data + data = self.command(0x4A, data, timeout=1.0) + return data[2:] if data and data[0] > 0 else None + + def in_atr(self, nfcid3i=b'', gi=b''): + flag = int(bool(nfcid3i)) | (int(bool(gi)) << 1) + data = bytearray([1, flag]) + nfcid3i + gi + data = self.command(0x50, data, timeout=1.5) + if data is None or data[0] != 0: + self.chipset_error(data) + return data[1:] + + def in_psl(self, br_it, br_ti): + data = bytearray([1, br_it, br_ti]) + data = self.command(0x4E, data, timeout=1.0) + if data is None or data[0] != 0: + self.chipset_error(data) + + def in_data_exchange(self, data, timeout, more=False): + data = self.command(0x40, bytearray([int(more) << 6 | 0x01]) + data, + timeout) + if data is None or data[0] & 0x3f != 0: + self.chipset_error(data[0] & 0x3f if data else None) + return data[1:], bool(data[0] & 0x40) + + def in_communicate_thru(self, data, timeout): + data = self.command(0x42, data, timeout) + if timeout > 0: + if data and data[0] == 0: + return data[1:] + else: + self.chipset_error(data) + + def tg_set_general_bytes(self, gb): + data = self.command(0x92, gb, timeout=0.1) + if data is None or data[0] != 0: + self.chipset_error(data) + + def tg_get_data(self, timeout): + data = self.command(0x86, b'', timeout) + if data is None or data[0] & 0x3f != 0: + self.chipset_error(data[0] & 0x3f if data else None) + return data[1:], bool(data[0] & 0x40) + + def tg_set_data(self, data, timeout): + data = self.command(0x8E, data, timeout) + if data is None or data[0] != 0: + self.chipset_error(data) + + def tg_set_meta_data(self, data, timeout): + data = self.command(0x94, data, timeout) + if data is None or data[0] != 0: + self.chipset_error(data) + + def tg_get_initiator_command(self, timeout): + data = self.command(0x88, b'', timeout) + if timeout > 0: + if data and data[0] == 0: + return data[1:] + else: + self.chipset_error(data) + + def tg_response_to_initiator(self, data): + data = self.command(0x90, data, timeout=1.0) + if data is None or data[0] != 0: + self.chipset_error(data) + + def tg_get_target_status(self): + data = self.command(0x8A, b'', timeout=0.1) + if data[0] == 0x01: + br_tx = (106, 212, 424)[data[1] >> 4 & 7] + br_rx = (106, 212, 424)[data[1] & 7] + else: + br_tx, br_rx = (0, 0) + return data[0], br_tx, br_rx + + +class Device(device.Device): + # Base class for devices with an NXP PN531, PN532, PN533 or Sony + # RC-S956 contactless interface chip. This class implements the + # functionality that is identical or needed by most of the drivers + # that inherit from pn53x. + + def __init__(self, chipset, logger): + self.chipset = chipset + self.log = logger + + try: + chipset_communication = self.chipset.diagnose('line') + except Chipset.Error: + chipset_communication = False + + if chipset_communication is False: + self.log.error("chipset communication test failed") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + # for line in self._print_ciu_register_page(0, 1, 2, 3): + # self.log.debug(line) + + # for addr in range(0, 0x03FF, 16): + # xram = self.chipset.read_register(*range(addr, addr+16)) + # xram = ' '.join(["%02X" % x for x in xram]) + # self.log.debug("0x%04X: %s", addr, xram) + + def close(self): + self.chipset.close() + self.chipset = None + + def mute(self): + self.chipset.rf_configuration(0x01, bytearray([0b00000010])) + + def sense_tta(self, target): + brty = {"106A": 0}.get(target.brty) + if brty not in self.chipset.in_list_passive_target_brty_range: + message = "unsupported bitrate {0}".format(target.brty) + self.log.warning(message) + raise ValueError(message) + + uid = target.sel_req if target.sel_req else bytearray() + if len(uid) > 4: + uid = b'\x88' + uid + if len(uid) > 8: + uid = uid[0:4] + b'\x88' + uid[4:] + + rsp = self.chipset.in_list_passive_target(1, 0, uid) + if rsp is not None: + sens_res, sel_res, sdd_res = rsp[1::-1], rsp[2:3], rsp[4:] + if sel_res[0] & 0x60 == 0x00: + self.log.debug("disable crc check for type 2 tag") + rxmode = self.chipset.read_register("CIU_RxMode") + self.chipset.write_register("CIU_RxMode", rxmode & 0x7F) + return nfc.clf.RemoteTarget( + "106A", sens_res=sens_res, sel_res=sel_res, sdd_res=sdd_res) + + if self.chipset.read_register("CIU_FIFOData") == 0x26: + # If we still see the SENS_REQ command in the CIU FIFO + # then there was no SENS_RES, thus no tag present. + return None + + self.log.debug("sens_res but no sdd_res, try as type 1 tag") + + if 4 not in self.chipset.in_list_passive_target_brty_range: + self.log.warning("The {0} can not read Type 1 Tags.".format(self)) + return None + + rsp = self.chipset.in_list_passive_target(1, 4, b"") + if rsp is not None: + rid_cmd = bytearray.fromhex("78 0000 00000000") + try: + rid_res = self.chipset.in_data_exchange(rid_cmd, 0.01)[0] + return nfc.clf.RemoteTarget( + "106A", sens_res=rsp[1::-1], rid_res=rid_res) + except Chipset.Error: + pass + + def sense_ttb(self, target, did=None): + brty = {"106B": 3, "212B": 6, "424B": 7, "848B": 8}.get(target.brty) + if brty not in self.chipset.in_list_passive_target_brty_range: + message = "unsupported bitrate {0}".format(target.brty) + self.log.warning(message) + raise ValueError(message) + + afi = target.sensb_req[0:1] if target.sensb_req else b'\x00' + rsp = self.chipset.in_list_passive_target(1, brty, afi) + if rsp and rsp[10] & 0b00001001 == 0b00000001: + # This is an ISO tag and the chipset has now activated it + # with 64-byte max frame size and maybe a DID. Because we + # implement ISO-DEP in software and can do without DID and + # use a full 256 byte response frame size, we'll send a + # DESELECT and WUPB to allow ATTRIB from the activation + # code in tags/tt4.py. + try: + deselect_command = (b'\xCA' + did) if did else b'\xC2' + wupb_command = b'\x05' + afi + b'\x08' + self.chipset.in_communicate_thru(deselect_command, 0.5) + rsp = self.chipset.in_communicate_thru(wupb_command, 0.5) + return nfc.clf.RemoteTarget(target.brty, sensb_res=rsp) + except (Chipset.Error, IOError) as error: + self.log.debug(error) + + def sense_ttf(self, target): + brty = {"212F": 1, "424F": 2}.get(target.brty) + if brty not in self.chipset.in_list_passive_target_brty_range: + message = "unsupported bitrate {0}".format(target.brty) + self.log.warning(message) + raise ValueError(message) + + if not self.chipset.read_register("CIU_TxControl") & 0b00000011: + # Some FeliCa cards need more time from power up to + # polling. If the field was not already activated, do this + # now and wait about 5 ms. + self.chipset.rf_configuration(0x01, b'\x01') + time.sleep(0.005) + + default_sensf_req = bytearray.fromhex("00FFFF0100") + sensf_req = target.sensf_req if target.sensf_req else default_sensf_req + rsp = self.chipset.in_list_passive_target(1, brty, sensf_req) + if rsp is not None: + return nfc.clf.RemoteTarget(target.brty, sensf_res=rsp[1:]) + + def sense_dep(self, target): + # Attempt active communication mode target activation. + assert target.atr_req, "the target.atr_req attribute is required" + assert len(target.atr_req) >= 16, "minimum lenght of atr_req is 16" + assert len(target.atr_req) <= 64, "maximum lenght of atr_req is 64" + + # bitrate and modulation type for send/recv must be set and equal + assert target.brty_send and target.brty_recv + assert target.brty_send == target.brty_recv + + br = int(target.brty[0:-1]) + nfcid3 = target.atr_req[2:12] + gbytes = target.atr_req[16:] + try: + data = self.chipset.in_jump_for_psl(1, br, b'', nfcid3, gbytes) + atr_res = b'\xD5\x01' + data + except Chipset.Error as error: + if error.errno not in (0x01, 0x0A): + self.log.error(error) + return None + finally: + # unset the detect-sync bit, 106A sync byte is handled in dep.py + self.chipset.write_register("CIU_Mode", 0b00111011) + + self.log.debug("running DEP in {0} kbps active mode".format(br)) + return nfc.clf.RemoteTarget(target.brty, atr_res=atr_res, + atr_req=target.atr_req) + + def get_max_send_data_size(self, target): + return self.chipset.host_command_frame_max_size - 2 + + def get_max_recv_data_size(self, target): + return self.chipset.host_command_frame_max_size - 3 + + def send_cmd_recv_rsp(self, target, data, timeout): + def bitrate(brty): + return [106 << i for i in range(6)].index(int(brty[:-1])) + + def framing(brty): + return {'A': 0b00, 'B': 0b11, 'F': 0b10}[brty[-1:]] + + # Set bitrate and modulation type for send and receive. + acm = target.atr_res and not (target.sens_res or target.sensf_res) + reg = ("CIU_TxMode", "CIU_RxMode", "CIU_TxAuto") + txm, rxm, txa = self.chipset.read_register(*reg) + txm = (txm & 0b10001111) | (bitrate(target.brty_send) << 4) + rxm = (rxm & 0b10001111) | (bitrate(target.brty_recv) << 4) + txm = (txm & 0b11111100) | (0b01 if acm else framing(target.brty_send)) + rxm = (rxm & 0b11111100) | (0b01 if acm else framing(target.brty_recv)) + txa = (txa & 0b10111111) | (target.brty_send.endswith("A") << 6) + reg = (("CIU_TxMode", txm), ("CIU_RxMode", rxm), ("CIU_TxAuto", txa)) + self.chipset.write_register(*reg) + + # Calculate the timeout index for InCommunicateThru. The + # effective timeout is T(us) = 100 * 2**(n-1) for 1 <= n <= 16 + # and "no timeout" for n = 0. For a given timeout we calculate + # the index as the first effective timeout that is longer. + timeout_microsec = int(timeout * 1E6) + try: + index = [i+1 for i in range(16) if timeout_microsec >> i <= 100][0] + except IndexError: + index = 16 + timeout_microsec = 100 << (index-1) + timeout = (100 << (index-1)) / 1E6 + self.log.log(logging.DEBUG-1, "set response timeout %.6f sec", timeout) + self.chipset.rf_configuration(0x02, bytearray([10, 11, index])) + + # Send the command data and return the response. All cases + # where a response is not received raise either an IOError + # or one of the nfc.clf.CommunicationError specializations. + data = bytearray(data) if not isinstance(data, bytearray) else data + try: + if target.sens_res and not target.atr_res: + if target.rid_res: # TT1 + return self._tt1_send_cmd_recv_rsp(data, timeout+0.1) + if target.sel_res[0] & 0x60 == 0x00: # TT2 + return self._tt2_send_cmd_recv_rsp(data, timeout+0.1) + return self.chipset.in_communicate_thru(data, timeout+0.1) + except Chipset.Error as error: + self.log.debug(error) + if error.errno == 1: + raise nfc.clf.TimeoutError + else: + raise nfc.clf.TransmissionError(str(error)) + except IOError as error: + self.log.debug(error) + if not error.errno == errno.ETIMEDOUT: + raise error + else: + raise nfc.clf.TimeoutError("send_cmd_recv_rsp") + + def _tt1_send_cmd_recv_rsp(self, data, timeout): + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError(cname + "._tt1_send_cmd_recv_rsp()") + + def _tt2_send_cmd_recv_rsp(self, data, timeout): + # The Type2Tag implementation needs to receive the Mifare + # ACK/NAK responses but the chipset reports them as crc error + # (indistinguishable from a real crc error). We thus have to + # switch off the crc check and do it here. + data = self.chipset.in_communicate_thru(data, timeout) + if len(data) > 2 and self.check_crc_a(data) is False: + raise nfc.clf.TransmissionError("crc_a check error") + return data[:-2] if len(data) > 2 else data + + def listen_tta(self, target, timeout): + if target.brty != "106A": + info = "unsupported bitrate/type: %r" % target.brty + raise nfc.clf.UnsupportedTargetError(info) + if target.rid_res: + info = "listening for type 1 tag activation is not supported" + raise nfc.clf.UnsupportedTargetError(info) + try: + assert target.sens_res is not None, "sens_res is required" + assert target.sdd_res is not None, "sdd_res is required" + assert target.sel_res is not None, "sel_res is required" + assert len(target.sens_res) == 2, "sens_res must be 2 byte" + assert len(target.sdd_res) == 4, "sdd_res must be 4 byte" + assert len(target.sel_res) == 1, "sel_res must be 1 byte" + assert target.sdd_res[0] == 0x08, "sdd_res[0] must be 08h" + except AssertionError as error: + raise ValueError(str(error)) + + nfcf_params = bytearray(range(18)) + nfca_params = target.sens_res + target.sdd_res[1:4] + target.sel_res + self.log.debug("nfca_params %s", hexlify(nfca_params).decode()) + + # We can use TgInitAsTarget to exclusively answer Type A + # activation when the CIU automatic mode detector is disabled + # (the firmware does not unset or even check this bit). When + # TgInitAsTarget prepares for AutoColl, the firmware also sets + # the CIU_TxMode and CIU_RXMode to 106A. + self.chipset.write_register("CIU_Mode", 0b00111111) + + time_to_return = time.time() + timeout + while time.time() < time_to_return: + try: + wait = max(time_to_return - time.time(), 0.5) + args = (1, nfca_params, nfcf_params, wait) + data = self._init_as_target(*args) + except IOError as error: + if error.errno != errno.ETIMEDOUT: + raise error + else: + return None + + brty = ("106A", "212F", "424F")[(data[0] & 0x70) >> 4] + self.log.debug("%s rcvd %s", + brty, hexlify(memoryview(data)[1:]).decode()) + if brty != target.brty or len(data) < 2: + log.debug("received bitrate does not match %s", target.brty) + continue + + if target.sel_res[0] & 0x60 == 0x00: + self.log.debug("rcvd TT2_CMD %s", + hexlify(memoryview(data)[1:]).decode()) + target = nfc.clf.LocalTarget(brty, tt2_cmd=data[1:]) + target.sens_res = nfca_params[0:2] + target.sdd_res = b'\x08' + nfca_params[2:5] + target.sel_res = nfca_params[5:6] + return target + + elif target.sel_res[0] & 0x20 == 0x20 and data[1] == 0xE0: + default_rats_res = bytearray.fromhex("05 78 80 70 02") + (rats_cmd, rats_res) = (data[1:], target.rats_res) + if not rats_res: + rats_res = default_rats_res + self.log.debug("rcvd RATS_CMD %s", hexlify(rats_cmd).decode()) + self.log.debug("send RATS_RES %s", hexlify(rats_res).decode()) + try: + self.chipset.tg_response_to_initiator(rats_res) + data = self.chipset.tg_get_initiator_command(1.0) + except (Chipset.Error, IOError) as error: + self.log.error(error) + return + if data and data[0] & 0xF0 == 0xC0: # S(DESELECT) + self.log.debug("rcvd S(DESELECT) %s", + hexlify(data).decode()) + self.log.debug("send S(DESELECT) %s", + hexlify(data).decode()) + self.chipset.tg_response_to_initiator(data) + elif data: + self.log.debug("rcvd TT4_CMD %s", + hexlify(data).decode()) + target = nfc.clf.LocalTarget(brty, tt4_cmd=data) + target.sens_res = nfca_params[0:2] + target.sdd_res = b'\x08' + nfca_params[2:5] + target.sel_res = nfca_params[5:6] + return target + + elif (target.sel_res[0] & 0x40 and data[1] == 0xF0 + and len(data) >= 19 and data[2] == len(data)-2 + and data[3:5] == b'\xD4\x00'): + self.log.debug("rcvd ATR_REQ %s", + hexlify(memoryview(data)[3:]).decode()) + target = nfc.clf.LocalTarget(brty, atr_req=data[3:]) + target.sens_res = nfca_params[0:2] + target.sdd_res = b'\x08' + nfca_params[2:5] + target.sel_res = nfca_params[5:6] + return target + + def listen_ttf(self, target, timeout): + # For NFC-F listen we can not use TgInitAsTarget because it + # always sets CIU_TxMode and CIU_RxMode to 106A. Best we can + # do is to program the CIU AutoColl command and then work with + # the CIU to receive tag commands in _tt3_send_rsp_recv_cmd + # (InCommunicateThru does not work probably because the + # firmware is not in target state). With the 64-bit only CIU + # FIFO it means that a tag can only allow two blocks for read + # and write. + if target.brty not in ("212F", "424F"): + info = "unsupported bitrate/type: %r" % target.brty + raise nfc.clf.UnsupportedTargetError(info) + try: + assert target.sensf_res is not None, "sensf_res is required" + assert len(target.sensf_res) == 19, "sensf_res must be 19 byte" + except AssertionError as error: + raise ValueError(str(error)) + + nfca_params = bytearray(6) + nfcf_params = bytearray(target.sensf_res[1:]) + self.log.debug("nfcf_params %s", hexlify(nfcf_params).decode()) + + regs = [ + ("CIU_Command", 0b00000000), # Idle command + ("CIU_FIFOLevel", 0b10000000), # clear fifo + ] + regs.extend(zip(25*["CIU_FIFOData"], + nfca_params + nfcf_params + b"\0")) + regs.append(("CIU_Command", 0b00000001)) # Configure command + self.chipset.write_register(*regs) + regs = [ + ("CIU_Control", 0b00000000), # act as target (b4=0) + ("CIU_Mode", 0b00111111), # disable mode detector (b2=1) + ("CIU_FelNFC2", 0b10000000), # wait until selected (b7=1) + ("CIU_TxMode", 0b10000010 | (int(target.brty[:-1])//212) << 4), + ("CIU_RxMode", 0b10001010 | (int(target.brty[:-1])//212) << 4), + ("CIU_TxControl", 0b10000000), # disable output on TX1/TX2 + ("CIU_TxAuto", 0b00100000), # wake up when rf level detected + ("CIU_Demod", 0b01100001), # use Q channel, freeze PLL in recv + ("CIU_CommIRq", 0b01111111), # clear interrupt request bits + ("CIU_DivIRq", 0b01111111), # clear interrupt request bits + ("CIU_Command", 0b00001101), # AutoColl command + ] + self.chipset.write_register(*regs) + + regs = ("CIU_Status1", "CIU_Status2", "CIU_CommIRq", "CIU_DivIRq") + time_to_return = time.time() + timeout + while time.time() < time_to_return: + time.sleep(0.01) + status1, status2, commirq, divirq \ + = self.chipset.read_register(*regs) + if commirq & 0b00110000 == 0b00110000: + self.chipset.write_register("CIU_CommIRq", 0b00110000) + fifo_size = self.chipset.read_register("CIU_FIFOLevel") + fifo_read = fifo_size * ["CIU_FIFOData"] + fifo_data = bytearray(self.chipset.read_register(*fifo_read)) + if fifo_data and len(fifo_data) == fifo_data[0]: + self.log.debug("%s rcvd %s", target.brty, + hexlify(fifo_data).decode()) + if fifo_data[2:10] == nfcf_params[0:8]: + target = nfc.clf.LocalTarget(target.brty) + target.sensf_res = b'\x01' + nfcf_params + target.tt3_cmd = fifo_data[1:] + return target + # Restart the AutoColl command. + self.chipset.write_register("CIU_Command", 0b00001101) + self.chipset.write_register("CIU_Command", 0) # Idle command + + def listen_dep(self, target, timeout): + assert target.sensf_res is not None + assert target.sens_res is not None + assert target.sdd_res is not None + assert target.sel_res is not None + assert target.atr_res is not None + + nfca_params = target.sens_res + target.sdd_res[1:4] + target.sel_res + nfcf_params = target.sensf_res[1:19] + self.log.debug("nfca_params %s", hexlify(nfca_params).decode()) + self.log.debug("nfcf_params %s", hexlify(nfcf_params).decode()) + assert len(nfca_params) == 6 + assert len(nfcf_params) == 18 + + # enable the automatic mode detector (b2 <= 0) + self.chipset.write_register( + ("CIU_Mode", 0b01111011), # b2 - enable mode detector + ("CIU_TxMode", 0b10110000), # 848 kbps Type A framing + ("CIU_RxMode", 0b10110000)) # 848 kbps Type A framing + + time_to_return = time.time() + timeout + while time.time() < time_to_return: + try: + wait = max(time_to_return - time.time(), 0.5) + data = self._init_as_target(2, nfca_params, nfcf_params, wait) + except IOError as error: + if error.errno != errno.ETIMEDOUT: + raise error + else: + if not (data[1] == len(data)-1 and data[2:4] == b'\xD4\x00'): + self.log.debug("expected ATR_REQ but got %s", + hexlify(memoryview(data)[1:]).decode()) + else: + break + else: + return + + brty = ("106A", "212F", "424F")[(data[0] & 0b01110000) >> 4] + mode = ("passive", "active")[data[0] & 1] + self.log.debug("activated in %s %s communication mode", brty, mode) + + atr_req = data[2:] + atr_res = target.atr_res[:] + atr_res[12] = atr_req[12] # copy DID + activation_params = ((nfca_params if brty == "106A" else nfcf_params) + if mode == "passive" else None) + + try: + self.log.debug("%s send ATR_RES %s", brty, + hexlify(atr_res).decode()) + data = self._send_atr_response(atr_res, timeout=1.0) + except Chipset.Error as error: + self.log.error(error) + return + except IOError as error: + if error.errno != errno.ETIMEDOUT: + raise + self.log.debug(error) + return + + psl_req = psl_res = None + if data and data.startswith(b'\x06\xD4\x04'): + self.log.debug("%s rcvd PSL_REQ %s", brty, + hexlify(memoryview(data)[1:]).decode()) + try: + psl_req = data[1:] + assert len(psl_req) == 5, "psl_req length mismatch" + assert psl_req[2] == atr_req[12], "psl_req has wrong did" + except AssertionError as error: + log.debug(str(error)) + return None + try: + psl_res = b'\xD5\x05' + psl_req[2:3] + self.log.debug("%s send PSL_RES %s", brty, + hexlify(psl_res).decode()) + brty = self._send_psl_response(psl_req, psl_res, timeout=0.5) + data = self.chipset.tg_get_initiator_command(timeout) + except Chipset.Error as error: + self.log.error(error) + return + except IOError as error: + if error.errno != errno.ETIMEDOUT: + raise + self.log.debug(error) + return + + if data and data[0] == len(data) and data[1:3] == b'\xD4\x06': + # set detect-sync bit to 0, the 106A sync byte is handled by dep.py + self.chipset.write_register("CIU_Mode", 0b00111011) + # prepare the target description to return, exact content + # depends on how we were activated (A or F with or w/o PSL) + target = nfc.clf.LocalTarget(brty, dep_req=data[1:]) + target.atr_req, target.atr_res = atr_req, atr_res + if psl_req: + target.psl_req = psl_req + if psl_res: + target.psl_res = psl_res + if activation_params == nfca_params: + target.sens_res = nfca_params[0:2] + target.sdd_res = b'\x08' + nfca_params[2:5] + target.sel_res = nfca_params[5:6] + if activation_params == nfcf_params: + target.sensf_res = b'\x01' + nfcf_params + return target + + def _init_as_target(self, mode, tta_params, ttf_params, timeout): + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError(cname + '._init_as_target()') + + def _send_atr_response(self, atr_res, timeout): + self.chipset.tg_response_to_initiator( + bytearray([len(atr_res)+1]) + atr_res) + return self.chipset.tg_get_initiator_command(timeout) + + def _send_psl_response(self, psl_req, psl_res, timeout): + dsi = psl_req[3] >> 3 & 0b111 + dri = psl_req[3] & 0b111 + rx_mode = self.chipset.read_register("CIU_RxMode") + rx_mode = (rx_mode & 0b10001111) | (dsi << 4) + if rx_mode & 0b00000011 != 1: # if not active mode + rx_mode = (rx_mode & 0b11111100) | ((0, 2)[dsi > 0]) + self.log.debug("set CIU_RxMode to {:08b}".format(rx_mode)) + self.chipset.write_register(("CIU_RxMode", rx_mode)) + self.log.debug("send PSL_RES %s", hexlify(psl_res).decode()) + data = bytearray([len(psl_res)+1]) + psl_res + self.chipset.tg_response_to_initiator(data) + tx_mode = self.chipset.read_register("CIU_TxMode") + tx_mode = (tx_mode & 0b10001111) | (dri << 4) + if tx_mode & 0b00000011 != 1: # if not active mode + tx_mode = (tx_mode & 0b11111100) | ((0, 2)[dri > 0]) + self.log.debug("set CIU_TxMode to {:08b}".format(tx_mode)) + self.chipset.write_register(("CIU_TxMode", tx_mode)) + return ("106A", "212F", "424F")[dri] + + def _tt3_send_rsp_recv_cmd(self, target, data, timeout): + regs = [ + ("CIU_FIFOLevel", 0b10000000), # clear fifo read/write pointer + ("CIU_CommIRq", 0b01111111), # clear interrupt request bits + ("CIU_DivIRq", 0b01111111), # clear interrupt request bits + ] + if data is not None: + regs.extend(zip(len(data)*["CIU_FIFOData"], data)) + regs.append(("CIU_BitFraming", 0b10000000)) # StartSend (b7=1) + self.chipset.write_register(*regs) + + irq_regs = ("CIU_CommIRq", "CIU_DivIRq") + time_to_return = time.time() + (timeout if timeout else 0) + while timeout is None or time.time() < time_to_return: + time.sleep(0.01) + commirq, divirq = self.chipset.read_register(*irq_regs) + if divirq & 0b00000001: + raise nfc.clf.BrokenLinkError("external field switched off") + if commirq & 0b00100000: + self.chipset.write_register("CIU_CommIRq", 0b00100000) + fifo_size = self.chipset.read_register("CIU_FIFOLevel") + fifo_read = fifo_size * ["CIU_FIFOData"] + fifo_data = bytearray(self.chipset.read_register(*fifo_read)) + if fifo_data[0] != len(fifo_data): + raise nfc.clf.TransmissionError("frame length byte error") + return fifo_data + if timeout > 0: + info = "no data received within %.3f s" % timeout + self.log.debug(info) + raise nfc.clf.TimeoutError(info) + + def send_rsp_recv_cmd(self, target, data, timeout): + # print("\n".join(self._print_ciu_register_page(0, 1))) + if target.tt3_cmd: + return self._tt3_send_rsp_recv_cmd(target, data, timeout) + try: + if data: + self.chipset.tg_response_to_initiator(data) + return self.chipset.tg_get_initiator_command(timeout) + except Chipset.Error as error: + if error.errno in (0x0A, 0x29, 0x31): + self.log.debug("Error: %s", error) + raise nfc.clf.BrokenLinkError(str(error)) + else: + self.log.warning(error) + raise nfc.clf.TransmissionError(str(error)) + except IOError as error: + if error.errno == errno.ETIMEDOUT: + info = "no data received within %.3f s" % timeout + self.log.debug(info) + raise nfc.clf.TimeoutError(info) + else: + # host-controller communication broken + self.log.error(error) + raise error + + def _print_ciu_register_page(self, *pages): + lines = list() + for page in pages: + base = (0x6331, 0x6301, 0x6311, 0x6321)[page] + regs = set(self.chipset.REG) + regs = sorted(regs.intersection(range(base, base+16))) + vals = self.chipset.read_register(*regs) + regs = [self.chipset.REG[r] for r in regs] + for r, v in zip(regs, vals): + lines.append("{0:16s} {1:08b}b {2:02X}h".format(r, v, v)) + return lines + + +def init(transport): + log.warning("pn53x is not a driver module, use pn531, pn532, or pn533") + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) diff --git a/src/lib/nfc/clf/rcs380.py b/src/lib/nfc/clf/rcs380.py new file mode 100644 index 0000000..95fcfe6 --- /dev/null +++ b/src/lib/nfc/clf/rcs380.py @@ -0,0 +1,986 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2012, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""Driver module for contactless devices based on the Sony NFC Port-100 +chipset. The only product known to use this chipset is the PaSoRi +RC-S380. The RC-S380 connects to the host as a native USB device. + +The RC-S380 has been the first NFC Forum certified device. It supports +reading and writing of all NFC Forum tags as well as peer-to-peer +mode. In addition, the NFC Port-100 also supports card emulation Type +A and Type F Technology. A notable restriction is that peer-to-peer +active communication mode (not required for NFC Forum certification) +is not supported. + +========== ======= ============ +function support remarks +========== ======= ============ +sense_tta yes +sense_ttb yes +sense_ttf yes +sense_dep no +listen_tta yes Type F responses can not be disabled +listen_ttb no +listen_ttf yes +listen_dep yes Only passive communication mode +========== ======= ============ + +""" +import nfc.clf +from . import device + +import time +import struct +import operator +from functools import reduce +from binascii import hexlify + +import logging +log = logging.getLogger(__name__) + + +class Frame(object): + def __init__(self, data): + self._data = None + self._type = None + self._frame = None + + if data[0:3] == bytearray(b"\x00\x00\xff"): + frame = bytearray(data) + if frame == bytearray(b"\x00\x00\xff\x00\xff\x00"): + self._type = "ack" + elif frame == bytearray(b"\x00\x00\xFF\xFF\xFF"): + self._type = "err" + elif frame[3:5] == bytearray(b"\xff\xff"): + self._type = "data" + if self.type == "data": + length = struct.unpack(" 0: + data = self.send_command(0x02, data) + if data and data[0] != 0: + raise StatusError(data[0]) + + def in_comm_rf(self, data, timeout): + timeout = min((timeout + (1 if timeout > 0 else 0)) * 10, 0xFFFF) + data = self.send_command(0x04, + struct.pack("Q", data[0:8])[0] + + def set_command_type(self, command_type): + data = self.send_command(0x2A, [command_type]) + if data and data[0] != 0: + raise StatusError(data[0]) + + +class Device(device.Device): + # Device driver for the Sony NFC Port-100 chipset. + + def __init__(self, chipset, logger): + self.chipset = chipset + self.log = logger + + minor, major = self.chipset.get_firmware_version() + self._chipset_name = "NFC Port-100 v{0:x}.{1:02x}".format(major, minor) + + def close(self): + self.chipset.close() + self.chipset = None + + def mute(self): + self.chipset.switch_rf("off") + + def sense_tta(self, target): + """Sense for a Type A Target is supported for 106, 212 and 424 + kbps. However, there may not be any target that understands the + activation commands in other than 106 kbps. + + """ + log.debug("polling for NFC-A technology") + + if target.brty not in ("106A", "212A", "424A"): + message = "unsupported bitrate {0}".format(target.brty) + raise nfc.clf.UnsupportedTargetError(message) + + self.chipset.in_set_rf(target.brty) + self.chipset.in_set_protocol(self.chipset.in_set_protocol_defaults) + self.chipset.in_set_protocol(initial_guard_time=6, add_crc=0, + check_crc=0, check_parity=1, + last_byte_bit_count=7) + + sens_req = (target.sens_req if target.sens_req else + bytearray.fromhex("26")) + + try: + sens_res = self.chipset.in_comm_rf(sens_req, 30) + if len(sens_res) != 2: + return None + except CommunicationError as error: + if error != "RECEIVE_TIMEOUT_ERROR": + log.debug(error) + return None + + log.debug("rcvd SENS_RES %s", hexlify(sens_res).decode()) + + if sens_res[0] & 0x1F == 0: + log.debug("type 1 tag target found") + self.chipset.in_set_protocol(last_byte_bit_count=8, add_crc=2, + check_crc=2, type_1_tag_rrdd=2) + target = nfc.clf.RemoteTarget(target.brty, sens_res=sens_res) + if sens_res[1] & 0x0F == 0b1100: + rid_cmd = bytearray.fromhex("78 0000 00000000") + log.debug("send RID_CMD %s", hexlify(rid_cmd).decode()) + try: + target.rid_res = self.chipset.in_comm_rf(rid_cmd, 30) + except CommunicationError as error: + log.debug(error) + return None + return target + + # other than type 1 tag + try: + self.chipset.in_set_protocol(last_byte_bit_count=8, add_parity=1) + if target.sel_req: + uid = target.sel_req + if len(uid) > 4: + uid = b"\x88" + uid + if len(uid) > 8: + uid = uid[0:4] + b"\x88" + uid[4:] + self.chipset.in_set_protocol(add_crc=1, check_crc=1) + for i, sel_cmd in zip(range(0, len(uid), 4), b"\x93\x95\x97"): + sel_req = bytearray([sel_cmd, 0x70]) + uid[i:i+4] + sel_req.append(reduce(operator.xor, sel_req[2:6])) # BCC + log.debug("send SEL_REQ %s", hexlify(sel_req).decode()) + sel_res = self.chipset.in_comm_rf(sel_req, 30) + log.debug("rcvd SEL_RES %s", hexlify(sel_res).decode()) + uid = target.sel_req + else: + uid = bytearray() + for sel_cmd in b"\x93\x95\x97": + self.chipset.in_set_protocol(add_crc=0, check_crc=0) + sdd_req = bytearray([sel_cmd, 0x20]) + log.debug("send SDD_REQ %s", hexlify(sdd_req).decode()) + sdd_res = self.chipset.in_comm_rf(sdd_req, 30) + log.debug("rcvd SDD_RES %s", hexlify(sdd_res).decode()) + self.chipset.in_set_protocol(add_crc=1, check_crc=1) + sel_req = bytearray([sel_cmd, 0x70]) + sdd_res + log.debug("send SEL_REQ %s", hexlify(sel_req).decode()) + sel_res = self.chipset.in_comm_rf(sel_req, 30) + log.debug("rcvd SEL_RES %s", hexlify(sel_res).decode()) + if sel_res[0] & 0b00000100: + uid = uid + sdd_res[1:4] + else: + uid = uid + sdd_res[0:4] + break + if sel_res[0] & 0b00000100 == 0: + return nfc.clf.RemoteTarget(target.brty, sens_res=sens_res, + sel_res=sel_res, sdd_res=uid) + except CommunicationError as error: + log.debug(error) + + def sense_ttb(self, target): + """Sense for a Type B Target is supported for 106, 212 and 424 + kbps. However, there may not be any target that understands the + activation command in other than 106 kbps. + + """ + log.debug("polling for NFC-B technology") + + if target.brty not in ("106B", "212B", "424B"): + message = "unsupported bitrate {0}".format(target.brty) + raise nfc.clf.UnsupportedTargetError(message) + + self.chipset.in_set_rf(target.brty) + self.chipset.in_set_protocol(self.chipset.in_set_protocol_defaults) + self.chipset.in_set_protocol(initial_guard_time=20, add_sof=1, + check_sof=1, add_eof=1, check_eof=1) + + sensb_req = (target.sensb_req if target.sensb_req else + bytearray.fromhex("050010")) + + log.debug("send SENSB_REQ %s", hexlify(sensb_req).decode()) + try: + sensb_res = self.chipset.in_comm_rf(sensb_req, 30) + except CommunicationError as error: + if error != "RECEIVE_TIMEOUT_ERROR": + log.debug(error) + return None + + if len(sensb_res) >= 12 and sensb_res[0] == 0x50: + log.debug("rcvd SENSB_RES %s", hexlify(sensb_res).decode()) + return nfc.clf.RemoteTarget(target.brty, sensb_res=sensb_res) + + def sense_ttf(self, target): + """Sense for a Type F Target is supported for 212 and 424 kbps. + + """ + log.debug("polling for NFC-F technology") + + if target.brty not in ("212F", "424F"): + message = "unsupported bitrate {0}".format(target.brty) + raise nfc.clf.UnsupportedTargetError(message) + + self.chipset.in_set_rf(target.brty) + self.chipset.in_set_protocol(self.chipset.in_set_protocol_defaults) + self.chipset.in_set_protocol(initial_guard_time=24) + + sensf_req = (target.sensf_req if target.sensf_req else + bytearray.fromhex("00FFFF0100")) + + log.debug("send SENSF_REQ %s", hexlify(sensf_req).decode()) + try: + frame = bytearray([len(sensf_req)+1]) + sensf_req + frame = self.chipset.in_comm_rf(frame, 10) + except CommunicationError as error: + if error != "RECEIVE_TIMEOUT_ERROR": + log.debug(error) + return None + + if 18 <= len(frame) == frame[0] and frame[1] == 1: + log.debug("rcvd SENSF_RES %s", hexlify(frame[1:]).decode()) + return nfc.clf.RemoteTarget(target.brty, sensf_res=frame[1:]) + + def sense_dep(self, target): + """Sense for an active DEP Target is not supported. The device only + supports passive activation via sense_tta/sense_ttf. + + """ + message = "{device} does not support sense for active DEP Target" + raise nfc.clf.UnsupportedTargetError(message.format(device=self)) + + def listen_tta(self, target, timeout): + """Listen as Type A Target in 106 kbps. + + Restrictions: + + * It is not possible to send short frames that are required + for ACK and NAK responses. This means that a Type 2 Tag + emulation can only implement a single sector memory model. + + * It can not be avoided that the chipset responds to SENSF_REQ + commands. The driver configures the SENSF_RES response to + all zero and ignores all Type F communication but eventually + it depends on the remote device whether Type A Target + activation will still be attempted. + + """ + if not target.brty == '106A': + info = "unsupported target bitrate: %r" % target.brty + raise nfc.clf.UnsupportedTargetError(info) + + if target.rid_res: + info = "listening for type 1 tag activation is not supported" + raise nfc.clf.UnsupportedTargetError(info) + + if target.sens_res is None: + raise ValueError("sens_res is required") + if target.sdd_res is None: + raise ValueError("sdd_res is required") + if target.sel_res is None: + raise ValueError("sel_res is required") + if len(target.sens_res) != 2: + raise ValueError("sens_res must be 2 byte") + if len(target.sdd_res) != 4: + raise ValueError("sdd_res must be 4 byte") + if len(target.sel_res) != 1: + raise ValueError("sel_res must be 1 byte") + if target.sdd_res[0] != 0x08: + raise ValueError("sdd_res[0] must be 08h") + + nfca_params = target.sens_res + target.sdd_res[1:4] + target.sel_res + log.debug("nfca_params %s", hexlify(nfca_params).decode()) + + self.chipset.tg_set_rf("106A") + self.chipset.tg_set_protocol(self.chipset.tg_set_protocol_defaults) + self.chipset.tg_set_protocol(rf_off_error=False) + + time_to_return = time.time() + timeout + tg_comm_rf_args = {'mdaa': True, 'nfca_params': nfca_params} + tg_comm_rf_args['recv_timeout'] = min(int(1000 * timeout), 0xFFFF) + + def listen_tta_tt2(): + recv_timeout = tg_comm_rf_args['recv_timeout'] + while recv_timeout > 0: + log.debug("wait %d ms for Type 2 Tag activation", recv_timeout) + try: + data = self.chipset.tg_comm_rf(**tg_comm_rf_args) + except CommunicationError as error: + log.debug(error) + else: + brty = ('106A', '212F', '424F')[data[0]-11] + log.debug("%s rcvd %s", + brty, hexlify(memoryview(data)[7:]).decode()) + if brty == "106A" and data[2] & 0x03 == 3: + self.chipset.tg_set_protocol(rf_off_error=True) + return nfc.clf.LocalTarget( + "106A", sens_res=nfca_params[0:2], + sdd_res=b'\x08'+nfca_params[2:5], + sel_res=nfca_params[5:6], tt2_cmd=data[7:]) + else: + log.debug("not a 106A Type 2 Tag command") + finally: + recv_timeout = int(1000 * (time_to_return - time.time())) + tg_comm_rf_args['recv_timeout'] = recv_timeout + + def listen_tta_tt4(): + rats_cmd = rats_res = None + recv_timeout = tg_comm_rf_args['recv_timeout'] + while recv_timeout > 0: + log.debug("wait %d ms for 106A TT4 command", recv_timeout) + try: + data = self.chipset.tg_comm_rf(**tg_comm_rf_args) + tg_comm_rf_args['transmit_data'] = None + except CommunicationError as error: + tg_comm_rf_args['transmit_data'] = None + rats_cmd = rats_res = None + log.debug(error) + else: + brty = ('106A', '212F', '424F')[data[0]-11] + log.debug("%s rcvd %s", brty, + hexlify(memoryview(data)[7:]).decode()) + if brty == "106A" and data[2] == 3 and data[7] == 0xE0: + (rats_cmd, rats_res) = (data[7:], target.rats_res) + log.debug("rcvd RATS_CMD %s", + hexlify(rats_cmd).decode()) + if rats_res is None: + rats_res = bytearray.fromhex("05 78 80 70 02") + log.debug("send RATS_RES %s", + hexlify(rats_res).decode()) + tg_comm_rf_args['transmit_data'] = rats_res + elif brty == "106A" and data[7] != 0xF0 and rats_cmd: + did = rats_cmd[1] & 0x0F + cmd = data[7:] + ta_tb_tc = rats_res[2:] + ta = ta_tb_tc.pop(0) if rats_res[1] & 0x10 else None + tb = ta_tb_tc.pop(0) if rats_res[1] & 0x20 else None + tc = ta_tb_tc.pop(0) if rats_res[1] & 0x40 else None + if ta is not None: + log.debug("TA(1) = {:08b}".format(ta)) + if tb is not None: + log.debug("TB(1) = {:08b}".format(tb)) + if tc is not None: + log.debug("TC(1) = {:08b}".format(tc)) + if ta_tb_tc: + log.debug("T({}) = {}".format( + len(ta_tb_tc), hexlify(ta_tb_tc).decode())) + did_supported = tc is None or bool(tc & 0x02) + cmd_with_did = bool(cmd[0] & 0x08) + if (((cmd_with_did and did_supported and cmd[1] == did) + or (did == 0 and not cmd_with_did))): + if cmd[0] in (0xC2, 0xCA): + log.debug("rcvd S(DESELECT) %s", + hexlify(cmd).decode()) + tg_comm_rf_args['transmit_data'] = cmd + log.debug("send S(DESELECT) %s", + hexlify(cmd).decode()) + rats_cmd = rats_res = None + else: + log.debug("rcvd TT4_CMD %s", + hexlify(cmd).decode()) + self.chipset.tg_set_protocol(rf_off_error=True) + return nfc.clf.LocalTarget( + "106A", sens_res=nfca_params[0:2], + sdd_res=b'\x08'+nfca_params[2:5], + sel_res=nfca_params[5:6], tt4_cmd=cmd, + rats_cmd=rats_cmd, rats_res=rats_res) + else: + log.debug("skip TT4_CMD %s (DID)", + hexlify(cmd).decode()) + else: + log.debug("not a 106A TT4 command") + finally: + recv_timeout = int(1000 * (time_to_return - time.time())) + tg_comm_rf_args['recv_timeout'] = recv_timeout + + if target.sel_res[0] & 0x60 == 0x00: + return listen_tta_tt2() + if target.sel_res[0] & 0x20 == 0x20: + return listen_tta_tt4() + + reason = "sel_res does not indicate any tag target support" + raise nfc.clf.UnsupportedTargetError(reason) + + def listen_ttb(self, target, timeout): + """Listen as Type B Target is not supported.""" + message = "{device} does not support listen as Type A Target" + raise nfc.clf.UnsupportedTargetError(message.format(device=self)) + + def listen_ttf(self, target, timeout): + """Listen as Type F Target is supported for either 212 or 424 kbps.""" + if target.brty not in ('212F', '424F'): + info = "unsupported target bitrate: %r" % target.brty + raise nfc.clf.UnsupportedTargetError(info) + + if target.sensf_res is None: + raise ValueError("sensf_res is required") + if len(target.sensf_res) != 19: + raise ValueError("sensf_res must be 19 byte") + + self.chipset.tg_set_rf(target.brty) + self.chipset.tg_set_protocol(self.chipset.tg_set_protocol_defaults) + self.chipset.tg_set_protocol(rf_off_error=False) + + recv_timeout = min(int(1000 * timeout), 0xFFFF) + time_to_return = time.time() + timeout + transmit_data = sensf_req = sensf_res = None + + while recv_timeout > 0: + if transmit_data: + log.debug("%s send %s", target.brty, + hexlify(transmit_data).decode()) + log.debug("%s wait recv %d ms", target.brty, recv_timeout) + try: + data = self.chipset.tg_comm_rf(recv_timeout=recv_timeout, + transmit_data=transmit_data) + except CommunicationError as error: + log.debug(error) + continue + finally: + recv_timeout = int((time_to_return - time.time()) * 1E3) + transmit_data = None + + assert target.brty == ('106A', '212F', '424F')[data[0]-11] + log.debug("%s rcvd %s", target.brty, + hexlify(memoryview(data)[7:]).decode()) + + if len(data) > 7 and len(data)-7 == data[7]: + if sensf_req and data[9:17] == target.sensf_res[1:9]: + self.chipset.tg_set_protocol(rf_off_error=True) + target = nfc.clf.LocalTarget(target.brty) + target.sensf_req = sensf_req + target.sensf_res = sensf_res + target.tt3_cmd = data[8:] + return target + + if len(data) == 13 and data[7] == 6 and data[8] == 0: + (sensf_req, sensf_res) = (data[8:], target.sensf_res[:]) + if (((sensf_req[1] == 255 or sensf_req[1] == sensf_res[17]) and + (sensf_req[2] == 255 or sensf_req[2] == sensf_res[18]))): + transmit_data = sensf_res[0:17] + if sensf_req[3] == 1: + transmit_data += sensf_res[17:19] + if sensf_req[3] == 2: + transmit_data += b"\x00" + transmit_data += bytearray( + [1 << (target.brty == "424F")]) + transmit_data = bytearray([len(transmit_data)+1]) \ + + transmit_data + + def listen_dep(self, target, timeout): + log.debug("listen_dep for {0:.3f} sec".format(timeout)) + + if not target.sens_res or len(target.sens_res) != 2: + raise ValueError("sens_res is required and must be 2 byte") + if not target.sel_res or len(target.sel_res) != 1: + raise ValueError("sel_res is required and must be 1 byte") + if not target.sdd_res or len(target.sdd_res) != 4: + raise ValueError("sdd_res is required and must be 4 byte") + if not target.sensf_res or len(target.sensf_res) < 19: + raise ValueError("sensf_res is required and must be 19 byte") + if not target.atr_res or len(target.atr_res) < 17: + raise ValueError("atr_res is required and must be >= 17 byte") + + nfca_params = target.sens_res + target.sdd_res[1:4] + target.sel_res + nfcf_params = target.sensf_res[1:19] + log.debug("nfca_params %s", hexlify(nfca_params).decode()) + log.debug("nfcf_params %s", hexlify(nfcf_params).decode()) + + self.chipset.tg_set_rf("106A") + self.chipset.tg_set_protocol(self.chipset.tg_set_protocol_defaults) + self.chipset.tg_set_protocol(rf_off_error=False) + + tg_comm_rf_args = {'mdaa': True} + tg_comm_rf_args['nfca_params'] = nfca_params + tg_comm_rf_args['nfcf_params'] = nfcf_params + + recv_timeout = min(int(1000 * timeout), 0xFFFF) + time_to_return = time.time() + timeout + + while recv_timeout > 0: + tg_comm_rf_args['recv_timeout'] = recv_timeout + log.debug("wait %d ms for activation", recv_timeout) + try: + data = self.chipset.tg_comm_rf(**tg_comm_rf_args) + except CommunicationError as error: + if error != "RECEIVE_TIMEOUT_ERROR": + log.warning(error) + else: + brty = ('106A', '212F', '424F')[data[0]-11] + log.debug("%s %s", brty, hexlify(data).decode()) + if data[2] & 0x03 == 3: + data = data[7:] + break + else: + log.debug("not a passive mode activation") + recv_timeout = int(1000 * (time_to_return - time.time())) + else: + return None + + # further tg_comm_rf commands return RF_OFF_ERROR when field is gone + self.chipset.tg_set_protocol(rf_off_error=True) + + if brty == "106A" and len(data) > 1 and data[0] != 0xF0: + # We received a Type A card activation, probably because + # sel_res has indicated Type 2 or Type 4A Tag support. + target = nfc.clf.LocalTarget("106A", tt2_cmd=data[:]) + target.sens_res = nfca_params[0:2] + target.sdd_res = b'\x08' + nfca_params[2:5] + target.sel_res = nfca_params[5:6] + return target + + def verify_frame(brty, data, cmd_set): + offset = 1 if brty == "106A" else 0 + try: + if brty == "106A" and data[0] != 0xF0: + log.warning("rcvd frame has invalid start byte") + elif data[offset] != len(data) - offset: + log.warning("rcvd frame has incorrect length byte") + elif data[offset+1] != 0xD4: + log.warning("rcvd frame command byte 1 is not D4h") + elif data[offset+2] not in cmd_set: + log.warning( + "rcvd frame command byte 2 not in %r" % cmd_set) + else: + return data[offset+1:] + except (IndexError): + log.warning("rcvd frame with less than header size") + + def send_res_recv_req(brty, data, timeout): + if data: + data = (b"", b"\xF0")[brty == "106A"] + \ + bytes([len(data)+1]) + data + args = {'transmit_data': data, 'recv_timeout': timeout} + data = self.chipset.tg_comm_rf(**args)[7:] + if timeout > 0: + return verify_frame(brty, data, cmd_set=[0, 4, 6, 8, 10]) + + activation_params = nfca_params if brty == '106A' else nfcf_params + data = verify_frame(brty, data, cmd_set=[0]) + + while data and data[1] == 0: + try: + (atr_req, atr_res) = (data[:], target.atr_res) + log.debug("%s rcvd ATR_REQ %s", + brty, hexlify(atr_req).decode()) + if 16 <= len(atr_req) <= 64: + log.debug("%s send ATR_RES %s", brty, + hexlify(atr_res).decode()) + data = send_res_recv_req(brty, atr_res, 1000) + else: + log.warning("ATR_REQ must be 16 to 64 byte") + data = None + except (CommunicationError) as error: + log.warning(str(error)) + data = None + + def send_dsl_res(brty, data): + dsl_res = b"\xD5\x09" + data[2:3] + log.debug("%s send DSL_RES %s", brty, hexlify(dsl_res).decode()) + send_res_recv_req(brty, dsl_res, 0) + + def send_rls_res(brty, data): + rls_res = b"\xD5\x0B" + data[2:3] + log.debug("%s send RLS_RES %s", brty, hexlify(rls_res).decode()) + send_res_recv_req(brty, rls_res, 0) + + def send_psl_res(brty, data): + (dsi, dri) = (data[3] >> 3 & 7, data[3] & 7) + if dsi != dri: + log.error("PSL_REQ DSI != DRI is not supported") + raise CommunicationError(b'\0\0\0\0') + (psl_req, psl_res) = (data[:], b"\xD5\x05" + data[2:3]) + log.debug("%s send PSL_RES %s", brty, hexlify(psl_res).decode()) + send_res_recv_req(brty, psl_res, 0) + brty = ('106A', '212F', '424F')[dsi] + self.chipset.tg_set_rf(brty) + return brty, psl_req, psl_res + + psl_req = None + while data and data[1] in (4, 6, 8, 10): + did = atr_req[12] if atr_req[12] > 0 else None + cmd = {4: "PSL", 6: "DEP", 8: "DSL", 10: "RLS"}.get(data[1], '???') + log.debug("%s rcvd %s_REQ %s", brty, cmd, hexlify(data).decode()) + try: + if cmd == "DEP": + if did == (data[3] if data[2] >> 2 & 1 else None): + target = nfc.clf.LocalTarget(brty, dep_req=data) + target.atr_req = atr_req + if psl_req: + target.psl_req = psl_req + if activation_params == nfca_params: + target.sens_res = nfca_params[0:2] + target.sdd_res = b'\x08' + nfca_params[2:5] + target.sel_res = nfca_params[5:6] + else: + target.sensf_res = b"\x01" + nfcf_params + return target + + elif cmd == "DSL": + if did == (data[2] if len(data) > 2 else None): + return send_dsl_res(brty, data) + + elif cmd == "RLS": + if did == (data[2] if len(data) > 2 else None): + return send_rls_res(brty, data) + + elif cmd == "PSL": # pragma: no branch + if did == (data[2] if data[2] > 0 else None): + brty, psl_req, psl_res = send_psl_res(brty, data) + + log.debug("%s wait recv 1000 ms", brty) + data = send_res_recv_req(brty, None, 1000) + + except (CommunicationError) as error: + log.warning(str(error)) + return None + + def get_max_send_data_size(self, target): + return 290 + + def get_max_recv_data_size(self, target): + return 290 + + def send_cmd_recv_rsp(self, target, data, timeout): + if timeout: + timeout_msec = max(min(int(timeout * 1000), 0xFFFF), 1) + else: + timeout_msec = 0 + self.chipset.in_set_rf(target.brty_send, target.brty_recv) + self.chipset.in_set_protocol(self.chipset.in_set_protocol_defaults) + in_set_protocol_settings = {} + if target.brty_send.endswith('A'): + in_set_protocol_settings['add_parity'] = 1 + in_set_protocol_settings['check_parity'] = 1 + if target.brty_send.endswith('B'): + in_set_protocol_settings['initial_guard_time'] = 20 + in_set_protocol_settings['add_sof'] = 1 + in_set_protocol_settings['check_sof'] = 1 + in_set_protocol_settings['add_eof'] = 1 + in_set_protocol_settings['check_eof'] = 1 + try: + if ((target.brty == '106A' and target.sel_res and + target.sel_res[0] & 0x60 == 0x00)): + # Driver must check TT2 CRC to get ACK/NAK + in_set_protocol_settings['check_crc'] = 0 + self.chipset.in_set_protocol(**in_set_protocol_settings) + return self._tt2_send_cmd_recv_rsp(data, timeout_msec) + else: + self.chipset.in_set_protocol(**in_set_protocol_settings) + return self.chipset.in_comm_rf(data, timeout_msec) + except CommunicationError as error: + log.debug(error) + if error == "RECEIVE_TIMEOUT_ERROR": + raise nfc.clf.TimeoutError + raise nfc.clf.TransmissionError + + def _tt2_send_cmd_recv_rsp(self, data, timeout_msec): + # The Type2Tag implementation needs to receive the Mifare + # ACK/NAK responses but the chipset reports them as crc error + # (indistinguishable from a real crc error). We thus had to + # switch off the crc check and do it here. + data = self.chipset.in_comm_rf(data, timeout_msec) + if len(data) > 2 and self.check_crc_a(data) is False: + raise nfc.clf.TransmissionError("crc_a check error") + return data[:-2] if len(data) > 2 else data + + def send_rsp_recv_cmd(self, target, data, timeout): + assert timeout is None or timeout >= 0 + kwargs = { + 'guard_time': 500, + 'transmit_data': data, + 'recv_timeout': 0xFFFF if timeout is None else int(timeout*1E3), + } + try: + data = self.chipset.tg_comm_rf(**kwargs) + return data[7:] if data else None + except CommunicationError as error: + log.debug(error) + if error == "RF_OFF_ERROR": + raise nfc.clf.BrokenLinkError(str(error)) + if error == "RECEIVE_TIMEOUT_ERROR": + raise nfc.clf.TimeoutError(str(error)) + raise nfc.clf.TransmissionError(str(error)) + + +def init(transport): + chipset = Chipset(transport, logger=log) + device = Device(chipset, logger=log) + device._vendor_name = transport.manufacturer_name + device._device_name = transport.product_name + return device diff --git a/src/lib/nfc/clf/rcs956.py b/src/lib/nfc/clf/rcs956.py new file mode 100644 index 0000000..7dbde12 --- /dev/null +++ b/src/lib/nfc/clf/rcs956.py @@ -0,0 +1,376 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""Driver for contacless devices based on the Sony RC-S956 +chipset. Products known to use this chipset are the PaSoRi RC-S330, +RC-S360, and RC-S370. The RC-S956 connects to the host as a native USB +device. + +The RC-S956 has the same hardware architecture as the NXP PN53x +family, i.e. it has a PN512 Contactless Interface Unit (CIU) coupled +with a 80C51 microcontroller and uses the same frame structure for +host communication and mostly the same commands. However, the firmware +that runs on the 80C51 is different and the most notable difference is +a much stricter state machine. The state machine restricts allowed +commands to certain modes. While direct access to the CIU registers is +possible, some of the things that can be done with a PN53x are +unfortunately prevented by the stricter state machine. + +========== ======= ============ +function support remarks +========== ======= ============ +sense_tta yes Only Type 1 Tags up to 128 byte (Topaz-96) +sense_ttb yes ATTRIB by firmware voided with S(DESELECT) +sense_ttf yes +sense_dep yes +listen_tta yes Only DEP and Type 2 Target +listen_ttb no +listen_ttf no +listen_dep yes Only passive communication mode +========== ======= ============ + +""" +import nfc.clf +from . import pn53x + +import time + +import logging +log = logging.getLogger(__name__) + + +class Chipset(pn53x.Chipset): + CMD = { + 0x00: "Diagnose", + 0x02: "GetFirmwareVersion", + 0x04: "GetGeneralStatus", + 0x06: "ReadRegister", + 0x08: "WriteRegister", + 0x0C: "ReadGPIO", + 0x10: "SetSerialBaudrate", + 0x12: "SetParameters", + 0x16: "PowerDown", + 0x32: "RFConfiguration", + 0x58: "RFRegulationTest", + 0x18: "ResetMode", + 0x1C: "ControlLED", + 0x56: "InJumpForDEP", + 0x46: "InJumpForPSL", + 0x4A: "InListPassiveTarget", + 0x50: "InATR", + 0x4E: "InPSL", + 0x40: "InDataExchange", + 0x42: "InCommunicateThru", + 0x44: "InDeselect", + 0x52: "InRelease", + 0x54: "InSelect", + 0x8C: "TgInitTarget", + 0x92: "TgSetGeneralBytes", + 0x86: "TgGetDEPData", + 0x8E: "TgSetDEPData", + 0x94: "TgSetMetaDEPData", + 0x88: "TgGetInitiatorCommand", + 0x90: "TgResponseToInitiator", + 0x8A: "TgGetTargetStatus", + 0xA0: "CommunicateThruEX", + } + ERR = { + 0x01: "Time out, the Target has not answered", + 0x02: "Checksum error during RF communication", + 0x03: "Parity error during RF communication", + 0x04: "Incorrect collision bit position in TargetID during SDD", + 0x07: "Overflow detected by the hardware during RF communication", + 0x0A: "RF field not activated in time by active mode peer", + 0x0B: "Protocol error during RF communication", + 0x0C: "More than 260 bytes payload received in ISO-DEP chaining", + 0x0D: "Overheated - antenna drivers deactivated", + 0x10: "Size of RF response packet during SDD was more than 4 bytes", + 0x13: "Format error during RF communication or retry count exceeded", + 0x14: "Authentication A or B failed for Type-A ISO target", + 0x17: "Unmatched block number in R(ACK) from ISO Type A or B card", + 0x23: "Invalid BCC value from ISO Type A card during anticollision", + 0x25: "TgGetDEPData or TgSetDEPData executed at wrong time", + 0x26: "PowerDown command received while USB interface being used", + 0x27: "Abnormal Tg parameter in the host command packet", + 0x29: "Release from the initiator in operation as DEPTarget", + 0x2A: "PUPI information in ATQB response differs from initial value", + 0x2B: "Failure to select a deselected target", + 0x2F: "Already deselected by the initiator in operation as DEPTarget", + 0x31: "Initiator RF-OFF state detected while operating as Target", + 0x32: "Buffer overflow detected by firmware during RF communication", + 0x34: "DEP_REQ(NACK) received but DEP_RES(INF) was never returned", + 0x35: "The received data exceeds LEN in the RF packet", + 0x7f: "Invalid command syntax - received error frame", + 0xfe: "A register write operation failed", + 0xff: "No data received from executing chip command", + } + + host_command_frame_max_size = 265 + in_list_passive_target_max_target = 1 + in_list_passive_target_brty_range = (0, 1, 2, 3, 4) + + def diagnose(self, test, test_data=None): + if test == "line": + size = self.host_command_frame_max_size - 3 + data = bytearray([x & 0xFF for x in range(size)]) + return self.command(0x00, b"\x00" + data, timeout=1.0) == data + return super(Chipset, self).diagnose(test, test_data) + + def _read_register(self, data): + # Max 64 registers can be read from RCS956 + assert len(data) <= 128 + return self.command(0x06, data, timeout=0.25) + + def _write_register(self, data): + # Max 64 registers can be written to RCS956 + assert len(data) <= 192 + status = self.command(0x08, data, timeout=0.25) + if sum(status) != 0: + self.chipset_error(0xfe) + + def reset_mode(self): + """Send a Reset command to set the operation mode to 0.""" + self.command(0x18, b"\x01", timeout=0.1) + self.transport.write(Chipset.ACK) + time.sleep(0.010) + + def tg_init_target(self, mode, mifare_params, felica_params, + nfcid3t, gt, timeout): + assert type(mode) is int and mode & 0b11111101 == 0 + assert len(mifare_params) == 6 + assert len(felica_params) == 18 + assert len(nfcid3t) == 10 + + data = bytearray([mode]) + mifare_params + felica_params + nfcid3t + gt + return self.command(0x8c, data, timeout) + + +class Device(pn53x.Device): + # Device driver for Sony RC-S956 based contactless devices. + + def __init__(self, chipset, logger): + assert isinstance(chipset, Chipset) + # Reset the RCS956 state machine to Mode 0. We may have left + # it in some other mode when an error has occured. + chipset.reset_mode() + + super(Device, self).__init__(chipset, logger) + + ic, ver, rev, support = self.chipset.get_firmware_version() + self._chipset_name = "RCS956v{0:x}.{1:x}".format(ver, rev) + self.log.debug("chipset is a {0}".format(self._chipset_name)) + + self.mute() + # Set timeout for PSL_RES, ATR_RES, InDataExchange/InCommunicateThru + self.chipset.rf_configuration(0x02, b"\x0B\x0B\x0A") + self.chipset.rf_configuration(0x04, b"\x00") + self.chipset.rf_configuration(0x05, b"\x00\x00\x01") + + self.log.debug("write rf settings for 106A") + data = bytearray.fromhex("5A F4 3F 11 4D 85 61 6F 26 62 87") + self.chipset.rf_configuration(0x0A, data) + + self.chipset.set_parameters(0b00001000) + self.chipset.reset_mode() + + # Set the RFCfg value for RAM-07. RF settings in RAM-07 are + # used for initial target state. During power-up RAM-07 is + # loaded from EEPROM-07 and the RFCfg value 0xFD stored in + # EEPROM-07 for RC-S330/360 prevents passive mode activation + # at 106A. It works with the RFCfg value 0x59 stored in ROM-07 + # (Neither value makes it work in active mode). + self.chipset.write_register(0x0328, 0x59) + + def close(self): + self.mute() + super(Device, self).close() + + def mute(self): + self.chipset.reset_mode() + super(Device, self).mute() + + def sense_tta(self, target): + """Activate the RF field and probe for a Type A Target. + + The RC-S956 can discover all Type A Targets (Type 1 Tag, Type + 2 Tag, and Type 4A Tag) at 106 kbps. Due to firmware + restrictions it is not possible to read a Type 1 Tag with + dynamic memory layout (more than 128 byte memory). + + """ + target = super(Device, self).sense_tta(target) + if target and target.rid_res: + # This is a TT1 tag. Unfortunately we can only read it if + # it is a static memory tag. The RCS956 has implemented + # the same wrong command codes as PN531/2/3 and directly + # programming the CIU does not work. + if target.rid_res[0] >> 4 == 1 and target.rid_res[0] & 15 != 1: + msg = "The {device} can not read this Type 1 Tag." + self.log.warning(msg.format(device=self)) + return None + return target + + def sense_ttb(self, target): + """Activate the RF field and probe for a Type B Target. + + The RC-S956 can discover Type B Targets (Type 4B Tag) at 106 + kbps. For a Type 4B Tag the firmware automatically sends an + ATTRIB command that configures the use of DID and 64 byte + maximum frame size. The driver reverts this configuration with + a DESELECT and WUPB command to return the target prepared for + activation (which nfcpy does in the tag activation code). + + """ + return super(Device, self).sense_ttb(target, did=b'\x01') + + def sense_ttf(self, target): + """Activate the RF field and probe for a Type F Target. + + """ + return super(Device, self).sense_ttf(target) + + def sense_dep(self, target): + """Search for a DEP Target in active or passive communication mode. + + """ + # Set timeout for PSL_RES and ATR_RES + self.chipset.rf_configuration(0x02, b"\x0B\x0B\x0A") + return super(Device, self).sense_dep(target) + + def listen_tta(self, target, timeout): + """Listen *timeout* seconds for a Type A activation at 106 kbps. The + ``sens_res``, ``sdd_res``, and ``sel_res`` response data must + be provided and ``sdd_res`` must be a 4 byte UID that starts + with ``08h``. Depending on ``sel_res`` an activation may + return a target with ``tt2_cmd`` or ``atr_req`` attribute. A + Type 4A Tag activation is not supported. + + """ + if target.sel_res and target.sel_res[0] & 0x20: + info = "{device} does not support listen as Type 4A Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + return super(Device, self).listen_tta(target, timeout) + + def listen_ttb(self, target, timeout): + """Listen as Type B Target is not supported.""" + info = "{device} does not support listen as Type B Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_ttf(self, target, timeout): + """Listen as Type F Target is not supported.""" + info = "{device} does not support listen as Type F Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_dep(self, target, timeout): + """Listen *timeout* seconds to become initialized as a DEP Target. + + The RC-S956 can be set to listen as a DEP Target for passive + communication mode. Target active communication mode is + disabled by the driver due to performance issues. It is also + not possible to fully control the ATR_RES response, only the + response waiting time (TO byte of ATR_RES) and the general + bytes can be set by the driver. Because the TO value must be + set before calling the hardware listen function, it can not be + different for the Type A of Type F passive initalization (the + driver uses the higher value if they are different). + + """ + # The RCS956 internal state machine must be in Mode 0 before + # we enter the listen phase. Also the RFConfiguration command + # for setting the TO parameter won't work in any other mode. + self.chipset.reset_mode() + + # Set the WaitForSelected bit in CIU_FelNFC2 register to + # prevent active mode activation. Target active mode is not + # really working with this device. + self.chipset.write_register("CIU_FelNFC2", 0x80) + + # We can not send ATR_RES as as a regular response but must + # use TgSetGeneralBytes to advance the chipset state machine + # to mode 3. Thus the ATR_RES is mostly determined by the + # firmware, we can only control the TO parameter for RWT, but + # must do it before the actual listen. + to = target.atr_res[15] & 0x0F + self.chipset.rf_configuration(0x82, bytearray([to, 2, to])) + + # Disable automatic ATR_RES transmission. This must be done + # all again because the chipset reactivates the setting after + # ATR_RES was once send in TgSetGeneralBytes. + self.chipset.set_parameters(0b00001000) + + # Now we can use the generic pn53x implementation + return super(Device, self).listen_dep(target, timeout) + + def _init_as_target(self, mode, tta_params, ttf_params, timeout): + nfcid3t = ttf_params[0:8] + b"\x00\x00" + args = (mode & 0xFE, tta_params, ttf_params, nfcid3t, b'', timeout) + return self.chipset.tg_init_target(*args) + + def _send_atr_response(self, atr_res, timeout): + # Before ATR_RES the device is in Mode 2 which does not allow + # the use of TgResponseToInitiator. To send the ATR_RES we + # must use TgSetGeneralBytes and can control only the general + # bytes and TO which we've set in _listen_dep(). We now copy + # the DID value from atr_req to atr_res but this will likely + # have no effect on the actual response. The hope is that the + # firmware will do the same when sending ATR_RES and we tell + # the truth to the caller. + self.log.debug("calling TgSetGeneralBytes to send ATR_RES") + self.chipset.tg_set_general_bytes(atr_res[17:]) + return self.chipset.tg_get_initiator_command(timeout) + + def _tt1_send_cmd_recv_rsp(self, data, timeout): + # Special handling for Tag Type 1 (Jewel/Topaz) card commands. + + if data[0] in (0x00, 0x01, 0x1A, 0x53, 0x72): + # RALL, READ, WRITE-NE, WRITE-E, RID are properly + # implemented by firmware. + return self.chipset.in_data_exchange(data, timeout)[0] + + # The other commands can not be executed. The workaround found + # for PN531, PN532 and PN533 fails with RCS956. While it is + # possible to properly send a TT1 command and the tag answers + # as expected, there is no way to get the response data from + # the CIU FIFO. For whatever reason the FIFO is empty, maybe + # the firmware constantly polls for new data and just removes + # it. That the response data was received can be guessed from + # the fact that the CIU Control register shows has the + # RxLastBits field set to exactly the correct number of valid + # bits in the last byte (when parity check is disabled, + # i.e. the FIFO contains one more bit for each received byte. + self.log.debug("tt1 command can not be send with this hardware ") + raise nfc.clf.TransmissionError("tt1 command can not be send") + + +def init(transport): + # Write ack to see if we can talk to the device. This raises + # IOError(EACCES) if it's claimed by some other process. + transport.write(Chipset.ACK) + + chipset = Chipset(transport, logger=log) + device = Device(chipset, logger=log) + + device._vendor_name = transport.manufacturer_name + device._device_name = transport.product_name + if device._device_name is None: + device._device_name = "RC-S330" + + return device diff --git a/src/lib/nfc/clf/transport.py b/src/lib/nfc/clf/transport.py new file mode 100644 index 0000000..b0eef8a --- /dev/null +++ b/src/lib/nfc/clf/transport.py @@ -0,0 +1,345 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2012, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +# +# Transport layer for host to reader communication. +# +import os +import re +import errno +from binascii import hexlify + +if not os.getenv("READTHEDOCS"): # pragma: no cover + try: + import usb1 as libusb + except ImportError: # pragma: no cover + raise ImportError("missing usb1 module, try 'pip install libusb1'") + +try: + import serial + import serial.tools.list_ports +except ImportError: # pragma: no cover + raise ImportError("missing serial module, try 'pip install pyserial'") + +try: + import termios +except ImportError: # pragma: no cover + assert os.name != 'posix' + +import logging +log = logging.getLogger(__name__) + +PATH = re.compile(r'^([a-z]+)(?::|)([a-zA-Z0-9-]+|)(?::|)([a-zA-Z0-9]+|)$') + + +class TTY(object): + TYPE = "TTY" + + @classmethod + def find(cls, path): + if not (path.startswith("tty") or path.startswith("com")): + return + + match = PATH.match(path) + + if match and match.group(1) == "tty": + if re.match(r'^(S|ACM|AMA|USB)\d+$', match.group(2)): + TTYS = re.compile(r'^tty{}$'.format(match.group(2))) + glob = False + elif re.match(r'^(S|ACM|AMA|USB)$', match.group(2)): + TTYS = re.compile(r'^tty{}\d+$'.format(match.group(2))) + glob = True + elif re.match(r'^usbserial-\w+$', match.group(2)): + TTYS = re.compile(r'^cu\.{}$'.format(match.group(2))) + glob = False + elif re.match(r'^usbserial$', match.group(2)): + TTYS = re.compile(r'^cu\.usbserial-.*$') + glob = True + elif re.match(r'^.+$', match.group(2)): + TTYS = re.compile(r'^{}$'.format(match.group(2))) + glob = False + else: + TTYS = re.compile(r'^(tty(S|ACM|AMA|USB)\d+|cu\.usbserial.*)$') + glob = True + + log.debug(TTYS.pattern) + ttys = [fn for fn in os.listdir('/dev') if TTYS.match(fn)] + + if len(ttys) > 0: + # Sort ttys with custom function to correctly order numbers. + ttys.sort(key=lambda item: (len(item), item)) + log.debug('check: ' + ' '.join('/dev/' + tty for tty in ttys)) + + # Eliminate tty nodes that are not physically present or + # inaccessible by the current user. Propagate IOError when + # path designated exactly one device, otherwise just log. + for i, tty in enumerate(ttys): + try: + termios.tcgetattr(open('/dev/%s' % tty)) + ttys[i] = '/dev/%s' % tty + except termios.error: + pass + except IOError as error: + log.debug(error) + if not glob: + raise error + + ttys = [tty for tty in ttys if tty.startswith('/dev/')] + log.debug('avail: %s', ' '.join([tty for tty in ttys])) + return ttys, match.group(3), glob + + if match and match.group(1) == "com": + if re.match(r'^COM\d+$', match.group(2)): + return [match.group(2)], match.group(3), False + if re.match(r'^\d+$', match.group(2)): + return ["COM" + match.group(2)], match.group(3), False + if re.match(r'^$', match.group(2)): + ports = [p[0] for p in serial.tools.list_ports.comports()] + log.debug('serial ports: %s', ' '.join(ports)) + return ports, match.group(3), True + log.error("invalid port in 'com' path: %r", match.group(2)) + + @property + def manufacturer_name(self): + return None + + @property + def product_name(self): + return None + + def __init__(self, port=None): + self.tty = None + self.open(port) + + def open(self, port, baudrate=115200): + self.close() + self.tty = serial.Serial(port, baudrate, timeout=0.05) + + @property + def port(self): + return self.tty.port if self.tty else '' + + @property + def baudrate(self): + return self.tty.baudrate if self.tty else 0 + + @baudrate.setter + def baudrate(self, value): + if self.tty: + self.tty.baudrate = value + + def read(self, timeout): + if self.tty is not None: + self.tty.timeout = max(timeout/1E3, 0.05) + frame = bytearray(self.tty.read(6)) + if frame is None or len(frame) == 0: + raise IOError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT)) + if frame.startswith(b"\x00\x00\xff\x00\xff\x00"): + log.log(logging.DEBUG-1, "<<< %s", hexlify(frame).decode()) + return frame + LEN = frame[3] + if LEN == 0xFF: + frame += self.tty.read(3) + LEN = frame[5] << 8 | frame[6] + frame += self.tty.read(LEN + 1) + log.log(logging.DEBUG-1, "<<< %s", hexlify(frame).decode()) + return frame + + def write(self, frame): + if self.tty is not None: + log.log(logging.DEBUG-1, ">>> %s", hexlify(frame).decode()) + self.tty.flushInput() + try: + self.tty.write(frame) + except serial.SerialTimeoutException: + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + def close(self): + if self.tty is not None: + self.tty.flushOutput() + self.tty.close() + self.tty = None + + +class USB(object): + TYPE = "USB" + + @classmethod + def find(cls, path): + if not path.startswith("usb"): + return + + log.debug("using libusb-{0}.{1}.{2}".format(*libusb.getVersion()[0:3])) + + usb_or_none = re.compile(r'^(usb|)$') + usb_vid_pid = re.compile(r'^usb(:[0-9a-fA-F]{4})(:[0-9a-fA-F]{4})?$') + usb_bus_dev = re.compile(r'^usb(:[0-9]{1,3})(:[0-9]{1,3})?$') + match = None + + for regex in (usb_vid_pid, usb_bus_dev, usb_or_none): + m = regex.match(path) + if m is not None: + log.debug("path matches {0!r}".format(regex.pattern)) + if regex is usb_vid_pid: + match = [int(s.strip(':'), 16) for s in m.groups() if s] + match = dict(zip(['vid', 'pid'], match)) + if regex is usb_bus_dev: + match = [int(s.strip(':'), 10) for s in m.groups() if s] + match = dict(zip(['bus', 'adr'], match)) + if regex is usb_or_none: + match = dict() + break + else: + return None + + with libusb.USBContext() as context: + devices = context.getDeviceList(skip_on_error=True) + vid, pid = match.get('vid'), match.get('pid') + bus, dev = match.get('bus'), match.get('adr') + if vid is not None: + devices = [d for d in devices if d.getVendorID() == vid] + if pid is not None: + devices = [d for d in devices if d.getProductID() == pid] + if bus is not None: + devices = [d for d in devices if d.getBusNumber() == bus] + if dev is not None: + devices = [d for d in devices if d.getDeviceAddress() == dev] + return [(d.getVendorID(), d.getProductID(), d.getBusNumber(), + d.getDeviceAddress()) for d in devices] + + def __init__(self, usb_bus, dev_adr): + self.context = libusb.USBContext() + self.open(usb_bus, dev_adr) + + def __del__(self): + self.close() + if self.context: # pragma: no branch + self.context.exit() + + def open(self, usb_bus, dev_adr): + self.usb_dev = None + self.usb_out = None + self.usb_inp = None + + for dev in self.context.getDeviceList(skip_on_error=True): + if ((dev.getBusNumber() == usb_bus and + dev.getDeviceAddress() == dev_adr)): + break + else: + log.error("no device {0} on bus {1}".format(dev_adr, usb_bus)) + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + try: + first_setting = next(dev.iterSettings()) + except StopIteration: + log.error("no usb configuration settings, please replug device") + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + def transfer_type(x): + return x & libusb.TRANSFER_TYPE_MASK + + def endpoint_dir(x): + return x & libusb.ENDPOINT_DIR_MASK + + for endpoint in first_setting.iterEndpoints(): + ep_addr = endpoint.getAddress() + ep_attr = endpoint.getAttributes() + if transfer_type(ep_attr) == libusb.TRANSFER_TYPE_BULK: + if endpoint_dir(ep_addr) == libusb.ENDPOINT_IN: + if not self.usb_inp: + self.usb_inp = endpoint + if endpoint_dir(ep_addr) == libusb.ENDPOINT_OUT: + if not self.usb_out: + self.usb_out = endpoint + + if not (self.usb_inp and self.usb_out): + log.error("no bulk endpoints for read and write") + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + try: + # workaround the PN533's buggy USB implementation + self._manufacturer_name = dev.getManufacturer() + self._product_name = dev.getProduct() + except libusb.USBErrorIO: + self._manufacturer_name = None + self._product_name = None + + try: + self.usb_dev = dev.open() + self.usb_dev.claimInterface(0) + except libusb.USBErrorAccess: + raise IOError(errno.EACCES, os.strerror(errno.EACCES)) + except libusb.USBErrorBusy: + raise IOError(errno.EBUSY, os.strerror(errno.EBUSY)) + except libusb.USBErrorNoDevice: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + def close(self): + if self.usb_dev: + self.usb_dev.close() + self.usb_dev = None + self.usb_out = None + self.usb_inp = None + + @property + def manufacturer_name(self): + return self._manufacturer_name + + @property + def product_name(self): + return self._product_name + + def read(self, timeout=0): + if self.usb_inp is not None: + try: + ep_addr = self.usb_inp.getAddress() + frame = self.usb_dev.bulkRead(ep_addr, 300, timeout) + except libusb.USBErrorTimeout: + raise IOError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT)) + except libusb.USBErrorNoDevice: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + except libusb.USBError as error: + log.error("%r", error) + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + if len(frame) == 0: + log.error("bulk read returned zero data") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + frame = bytearray(frame) + log.log(logging.DEBUG-1, "<<< %s", hexlify(frame).decode()) + return frame + + def write(self, frame, timeout=0): + if self.usb_out is not None: + log.log(logging.DEBUG-1, ">>> %s", hexlify(frame).decode()) + try: + ep_addr = self.usb_out.getAddress() + self.usb_dev.bulkWrite(ep_addr, bytes(frame), timeout) + if len(frame) % self.usb_out.getMaxPacketSize() == 0: + self.usb_dev.bulkWrite(ep_addr, b'', timeout) + except libusb.USBErrorTimeout: + raise IOError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT)) + except libusb.USBErrorNoDevice: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + except libusb.USBError as error: + log.error("%r", error) + raise IOError(errno.EIO, os.strerror(errno.EIO)) diff --git a/src/lib/nfc/clf/udp.py b/src/lib/nfc/clf/udp.py new file mode 100644 index 0000000..dbb629b --- /dev/null +++ b/src/lib/nfc/clf/udp.py @@ -0,0 +1,577 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2012, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""Driver module for simulated contactless communication over +UDP/IP. It can be activated with the device path ``udp::`` +where the optional *host* may be the IP address or name of the node +where the targeted communication partner is listening on *port*. The +default values for *host* and *port* are ``localhost:54321``. + +The driver implements almost all communication modes, with the current +exception of active communication mode data exchange protocol. + +========== ======= ============ +function support remarks +========== ======= ============ +sense_tta yes +sense_ttb yes +sense_ttf yes +sense_dep no +listen_tta yes +listen_ttb yes +listen_ttf yes +listen_dep yes +========== ======= ============ + +""" +import nfc.clf + +import time +import errno +import socket +import select +import operator +from functools import reduce +from binascii import hexlify, unhexlify + +import logging +log = logging.getLogger(__name__) + + +class Device(nfc.clf.device.Device): + def __init__(self, host, port): + host = socket.gethostbyname(host) + host, port = socket.getnameinfo((host, port), socket.NI_NUMERICHOST) + self.addr = (host, int(port)) + self._path = "%s:%s" % (host, port) + self.socket = None + self._create_socket() + + def close(self): + self.mute() + + def mute(self): + if self.socket: + # send RFOFF when socket port != listen port + if self.socket.getsockname()[1] != self.addr[1] and self.rcvd_data: + self._send_data("RFOFF", b"", self.addr) + self.socket.close() + self.socket = None + + def sense_tta(self, target): + self._create_socket() + + log.debug("sense_tta for %s on %s:%d", target, *self.addr) + + if target.brty not in ("106A", "212A", "424A"): + message = "unsupported bitrate {0}".format(target.brty) + raise nfc.clf.UnsupportedTargetError(message) + + sens_req = (target.sens_req if target.sens_req else + bytearray.fromhex("26")) + + log.debug("send SENS_REQ %s", hexlify(sens_req).decode()) + try: + self._send_data(target.brty, sens_req, self.addr) + brty, sens_res, addr = self._recv_data(1.0, target.brty) + except nfc.clf.TimeoutError: + return None + + log.debug("rcvd SENS_RES %s", hexlify(sens_res).decode()) + + if sens_res[0] & 0x1F == 0: + log.debug("type 1 tag target found") + target = nfc.clf.RemoteTarget(target.brty, _addr=addr) + target.sens_res = sens_res + if sens_res[1] & 0x0F == 0b1100: + rid_cmd = bytearray.fromhex("78 0000 00000000") + log.debug("send RID_CMD %s", hexlify(rid_cmd).decode()) + try: + self._send_data(brty, rid_cmd, self.addr) + brty, rid_res, addr = self._recv_data(1.0, brty) + target.rid_res = rid_res + except nfc.clf.CommunicationError as error: + log.debug(error) + return None + return target + + # other than type 1 tag + try: + if target.sel_req: + uid = target.sel_req + if len(uid) > 4: + uid = b"\x88" + uid + if len(uid) > 8: + uid = uid[0:4] + b"\x88" + uid[4:] + for i, sel_cmd in zip(range(0, len(uid), 4), b"\x93\x95\x97"): + sel_req = bytearray([sel_cmd, 0x70]) + uid[i:i+4] + sel_req.append(reduce(operator.xor, sel_req[2:6])) # BCC + log.debug("send SEL_REQ {}".format( + hexlify(sel_req).decode())) + self._send_data(brty, sel_req, addr) + brty, sel_res, addr = self._recv_data(0.5, brty) + log.debug("rcvd SEL_RES {}".format( + hexlify(sel_res).decode())) + uid = target.sel_req + else: + uid = bytearray() + for sel_cmd in b"\x93\x95\x97": + sdd_req = bytearray([sel_cmd, 0x20]) + log.debug("send SDD_REQ {}".format( + hexlify(sdd_req).decode())) + self._send_data(brty, sdd_req, addr) + brty, sdd_res, addr = self._recv_data(0.5, brty) + log.debug("rcvd SDD_RES {}".format( + hexlify(sdd_res).decode())) + sel_req = bytearray([sel_cmd, 0x70]) + sdd_res + log.debug("send SEL_REQ {}".format( + hexlify(sel_req).decode())) + self._send_data(brty, sel_req, addr) + brty, sel_res, addr = self._recv_data(0.5, brty) + log.debug("rcvd SEL_RES {}".format( + hexlify(sel_res).decode())) + if sel_res[0] & 0b00000100: + uid = uid + sdd_res[1:4] + else: + uid = uid + sdd_res[0:4] + break + if sel_res[0] & 0b00000100 == 0: + target = nfc.clf.RemoteTarget(target.brty, _addr=addr) + target.sens_res = sens_res + target.sel_res = sel_res + target.sdd_res = uid + return target + except nfc.clf.CommunicationError as error: + log.debug(error) + + def sense_ttb(self, target): + self._create_socket() + + if target.brty not in ("106B", "212B", "424B"): + message = "unsupported bitrate {0}".format(target.brty) + raise nfc.clf.UnsupportedTargetError(message) + + sensb_req = (target.sensb_req if target.sensb_req else + bytearray.fromhex("050010")) + + log.debug("send SENSB_REQ %s", hexlify(sensb_req).decode()) + try: + self._send_data(target.brty, sensb_req, self.addr) + brty, sensb_res, addr = self._recv_data(1.0, target.brty) + except nfc.clf.CommunicationError: + return None + + if len(sensb_res) >= 12 and sensb_res[0] == 0x50: + log.debug("rcvd SENSB_RES %s", hexlify(sensb_res).decode()) + return nfc.clf.RemoteTarget(brty, sensb_res=sensb_res, _addr=addr) + + def sense_ttf(self, target): + self._create_socket() + + log.debug("sense_ttf for %s on %s:%d", target, *self.addr) + + if target.brty not in ("212F", "424F"): + message = "unsupported bitrate {0}".format(target.brty) + raise nfc.clf.UnsupportedTargetError(message) + + if not target.sensf_req: + sensf_req = bytearray.fromhex("0600FFFF0100") + else: + sensf_req = bytearray([len(target.sensf_req)+1]) + target.sensf_req + + log.debug("send SENSF_REQ {}".format( + hexlify(memoryview(sensf_req)[1:]).decode())) + try: + self._send_data(target.brty, sensf_req, self.addr) + brty, data, addr = self._recv_data(1.0, target.brty) + except nfc.clf.CommunicationError: + return None + + if len(data) >= 18 and data[0] == len(data) and data[1] == 1: + log.debug("rcvd SENSF_RES %s", hexlify(data[1:]).decode()) + return nfc.clf.RemoteTarget(brty, sensf_res=data[1:], _addr=addr) + + def sense_dep(self, target): + info = "{device} does not support sense for active DEP Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_tta(self, target, timeout): + self._create_socket() + + log.debug("listen_tta for %.3f seconds on %s:%d", timeout, *self.addr) + + time_to_return = time.time() + timeout + if not self._bind_socket(time_to_return): + log.debug("failed to bind socket") + return None + + log.debug("wait for data on socket %s:%d", *self.socket.getsockname()) + return self._listen_tta(target, time_to_return) + + def _listen_tta(self, target, time_to_return, init=None): + sdd_res = bytearray(target.sdd_res) + if len(sdd_res) > 4: + sdd_res.insert(0, 0x88) + if len(sdd_res) > 8: + sdd_res.insert(4, 0x88) + sdd_res.insert(4, reduce(operator.xor, sdd_res[0:4])) + if len(sdd_res) > 5: + sdd_res.insert(9, reduce(operator.xor, sdd_res[5:9])) + if len(sdd_res) > 10: + sdd_res.insert(14, reduce(operator.xor, sdd_res[10:14])) + sel_res = bytearray([target.sel_res[0] & 0b11111011]) + + while time.time() < time_to_return: + if init is None: + wait = max(0.5, time_to_return - time.time()) + try: + brty, data, addr = self._recv_data(wait, target.brty) + except nfc.clf.TimeoutError: + return None + except nfc.clf.CommunicationError: + continue + else: + (brty, data, addr), init = init, None + if data == b"\x26": + log.debug("rcvd SENS_REQ %s", hexlify(data).decode()) + sens_res = target.sens_res + log.debug("send SENS_RES %s", hexlify(sens_res).decode()) + self._send_data(brty, sens_res, addr) + elif data == b"\x93\x20": + log.debug("rcvd SDD_REQ CL1 %s", hexlify(data).decode()) + log.debug("send SDD_RES CL1 %s", + hexlify(sdd_res[0:5]).decode()) + self._send_data(brty, sdd_res[0:5], addr) + elif data == b"\x95\x20" and len(sdd_res) > 5: + log.debug("rcvd SDD_REQ CL2 %s", hexlify(data).decode()) + log.debug("send SDD_RES CL2 %s", + hexlify(sdd_res[5:10]).decode()) + self._send_data(brty, sdd_res[5:10], addr) + elif data == b"\x97\x20" and len(sdd_res) > 10: + log.debug("rcvd SDD_REQ CL3 %s", hexlify(data).decode()) + log.debug("send SDD_RES CL3 %s", + hexlify(sdd_res[10:15]).decode()) + self._send_data(brty, sdd_res[10:15], addr) + elif data == b"\x93\x70" + sdd_res[0:5]: + log.debug("rcvd SEL_REQ Cl1 %s", hexlify(data).decode()) + sel_res[0] = (sel_res[0] & 0xFB) | (len(sdd_res) > 5) << 2 + log.debug("send SEL_RES %s", hexlify(sel_res).decode()) + self._send_data(brty, sel_res, addr) + elif data == b"\x95\x70" + sdd_res[5:10]: + log.debug("rcvd SEL_REQ CL2 %s", hexlify(data).decode()) + sel_res[0] = (sel_res[0] & 0xFB) | (len(sdd_res) > 10) << 2 + log.debug("send SEL_RES %s", hexlify(sel_res).decode()) + self._send_data(brty, sel_res, addr) + elif data == b"\x95\x70" + sdd_res[10:15]: + log.debug("rcvd SEL_REQ CL3 %s", hexlify(data).decode()) + sel_res[0] = (sel_res[0] & 0xFB) | (len(sdd_res) > 15) << 2 + log.debug("send SEL_RES %s", hexlify(sel_res).decode()) + self._send_data(brty, sel_res, addr) + elif sel_res[0] & 0b00000100 == 0: + target = nfc.clf.LocalTarget( + brty, _addr=addr, sens_res=target.sens_res, + sdd_res=target.sdd_res, sel_res=target.sel_res) + if ((data[0] == 0xF0 and len(data) >= 18 and + data[1] == len(data)-1 and data[2:4] == b"\xD4\x00")): + target.atr_req = data[2:] + elif data[0] == 0xE0: + target.tt4_cmd = data[:] + else: + target.tt2_cmd = data[:] + return target + + def listen_ttb(self, target, timeout): + self._create_socket() + + log.debug("listen_ttb for %.3f seconds on %s:%d", timeout, *self.addr) + + time_to_return = time.time() + timeout + if not self._bind_socket(time_to_return): + log.debug("failed to bind socket") + return None + + assert target.sensb_res and len(target.sensb_res) >= 12 + log.debug("wait for data on socket %s:%d", *self.socket.getsockname()) + + while time.time() < time_to_return: + wait = max(0.5, time_to_return - time.time()) + try: + brty, data, addr = self._recv_data(wait, target.brty) + except nfc.clf.TimeoutError: + return None + except nfc.clf.CommunicationError: + continue + if data and len(data) == 3 and data.startswith(b'\x05'): + req = "ALLB_REQ" if data[1] & 0x08 else "SENSB_REQ" + sensb_req = data + log.debug("rcvd %s %s", req, hexlify(sensb_req).decode()) + log.debug("send SENSB_RES %s", + hexlify(target.sensb_res).decode()) + self._send_data(brty, target.sensb_res, addr) + brty, data, addr = self._recv_data(wait, target.brty) + return nfc.clf.LocalTarget(brty, sensb_req=sensb_req, + sensb_res=target.sensb_res, + tt4_cmd=data, _addr=addr) + + def listen_ttf(self, target, timeout): + self._create_socket() + + log.debug("listen_ttf for %.3f seconds on %s:%d", timeout, *self.addr) + + time_to_return = time.time() + timeout + if not self._bind_socket(time_to_return): + log.debug("failed to bind socket") + return None + + log.debug("wait for data on socket %s:%d", *self.socket.getsockname()) + return self._listen_ttf(target, time_to_return) + + def _listen_ttf(self, target, time_to_return, init=None): + sensf_req = sensf_res = None + while time.time() < time_to_return: + if init is None: + wait = max(0.5, time_to_return - time.time()) + try: + brty, data, addr = self._recv_data(wait, target.brty) + except nfc.clf.TimeoutError: + return None + except nfc.clf.CommunicationError: + continue + else: + (brty, data, addr), init = init, None + if data and len(data) == data[0]: + if data.startswith(b"\x06\x00"): + (sensf_req, sensf_res) = (data[1:], target.sensf_res[:]) + if (((sensf_req[1] == 255 or + sensf_req[1] == sensf_res[17]) and + (sensf_req[2] == 255 or + sensf_req[2] == sensf_res[18]))): + data = sensf_res[0:17] + if sensf_req[3] == 1: + data += sensf_res[17:19] + if sensf_req[3] == 2: + data += bytearray( + [0x00, 1 << (target.brty == "424F")]) + data = bytearray([len(data)+1]) + data + self._send_data(brty, data, addr) + else: + sensf_req = sensf_res = None + elif sensf_req and sensf_res: + if data[2:10] == target.sensf_res[1:9]: + target = nfc.clf.LocalTarget(brty, _addr=addr) + target.sensf_req = sensf_req + target.sensf_res = sensf_res + target.tt3_cmd = data[1:] + return target + if data[1:11] == b'\xD4\x00' + target.sensf_res[1:9]: + target = nfc.clf.LocalTarget(brty, _addr=addr) + target.sensf_req = sensf_req + target.sensf_res = sensf_res + target.atr_req = data[1:] + return target + + def listen_dep(self, target, timeout): + self._create_socket() + + log.debug("listen_dep for %.3f seconds on %s:%d", timeout, *self.addr) + assert target.sensf_res is not None + assert target.sens_res is not None + assert target.sdd_res is not None + assert target.sel_res is not None + assert target.atr_res is not None + assert len(target.sensf_res) == 19 + assert len(target.sens_res) == 2 + assert len(target.sdd_res) == 4 + assert len(target.sel_res) == 1 + assert len(target.atr_res) >= 17 and len(target.atr_res) <= 64 + + time_to_return = time.time() + timeout + if not self._bind_socket(time_to_return): + log.debug("failed to bind socket") + return None + + log.debug("wait for data on socket %s:%d", *self.socket.getsockname()) + atr_res = bytearray(target.atr_res) + + while time.time() < time_to_return: + wait = max(0, time_to_return - time.time()) + try: + result = self._recv_data(wait, '106A', '212F', '424F') + brty, data, addr = result + except nfc.clf.CommunicationError: + return None + + target.brty = brty + if brty == '106A': + if data == b"\x26": + init = (brty, data, addr) + target = self._listen_tta(target, time_to_return, init) + elif (len(data) >= 18 and data[1] == len(data)-1 and + data[0] == 0xF0 and data[2:4] == b'\xD4\x00'): + target = nfc.clf.LocalTarget( + brty, atr_res=target.atr_res, atr_req=data[2:]) + elif brty in ('212F', '424F') and data[0] == len(data): + if data.startswith(b'\x06\x00'): + init = (brty, data, addr) + target = self._listen_ttf(target, time_to_return, init) + elif len(data) >= 17 and data[1:3] == b'\xD4\x00': + target = nfc.clf.LocalTarget( + brty, atr_res=target.atr_res, atr_req=data[1:]) + + if target and target.atr_req: + target.atr_res = atr_res + log.debug("rcvd ATR_REQ %s", hexlify(target.atr_req).decode()) + log.debug("send ATR_RES %s", hexlify(target.atr_res).decode()) + data = bytearray([len(atr_res) + 1]) + atr_res + if brty == '106A': + data.insert(0, 0xF0) + self._send_data(brty, data, addr) + brty, data, addr = self._recv_data(wait, brty) + try: + if brty == '106A': + assert data.pop(0) == 0xF0 + assert len(data) == data.pop(0) + except AssertionError: + return None + if data.startswith(b'\xD4\x04'): + target.psl_req = data[:] + target.psl_res = b'\xD5\x05' + target.psl_req[2:3] + log.debug("rcvd PSL_REQ %s", + hexlify(target.psl_req).decode()) + log.debug("send PSL_RES %s", + hexlify(target.psl_res).decode()) + data = bytearray([len(target.psl_res) + 1]) \ + + target.psl_res + if brty == '106A': + data.insert(0, 0xF0) + self._send_data(brty, data, addr) + brty = ('106A', '212F', '424F')[target.psl_req[3] >> 3 & 7] + target.brty, data, addr = self._recv_data(wait, brty) + try: + if brty == '106A': + assert data.pop(0) == 0xF0 + assert len(data) == data.pop(0) + except AssertionError: + return None + if data.startswith(b'\xD4\x08'): + log.debug("rcvd DSL_REQ %s", hexlify(data).decode()) + data = b'\xD5\x09' + data[2:3] + log.debug("send DSL_RES %s", hexlify(data).decode()) + data = bytearray([len(data) + 1]) + data + if brty == '106A': + data.insert(0, 0xF0) + self._send_data(brty, data, addr) + return None + if data.startswith(b'\xD4\x0A'): + log.debug("rcvd RLS_REQ %s", hexlify(data).decode()) + data = b'\xD5\x0B' + data[2:3] + log.debug("send RLS_RES %s", hexlify(data).decode()) + data = bytearray([len(data) + 1]) + data + if brty == '106A': + data.insert(0, 0xF0) + self._send_data(brty, data, addr) + return None + if data.startswith(b'\xD4\x06'): + target.dep_req = data[:] + return target + return None + + def send_cmd_recv_rsp(self, target, data, timeout): + # send data, data should normally not be None for the Initiator + if data is not None: + self._send_data(target.brty, data, target._addr) + + # receive response data unless the timeout is zero + if timeout > 0: + brty, data, addr = self._recv_data(timeout, target.brty) + return data + + def send_rsp_recv_cmd(self, target, data, timeout): + # send data, data may be none as target keeps silence on error + if data is not None: + self._send_data(target.brty, data, target._addr) + + # recv response data unless the timeout is zero + if timeout is None or timeout > 0: + brty, data, addr = self._recv_data(timeout, target.brty) + return data + + def get_max_send_data_size(self, target): + return 290 + + def get_max_recv_data_size(self, target): + return 290 + + def _create_socket(self): + if self.socket is None: + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.sent_data = self.rcvd_data = 0 + + def _bind_socket(self, time_to_return): + addr = ('0.0.0.0', self.addr[1]) + while time.time() < time_to_return: + log.debug("trying to bind socket to %s:%d", *addr) + try: + self.socket.bind(addr) + return True + except socket.error as error: + log.debug("bind failed with %s", error) + if error.errno == errno.EADDRINUSE: + return False + else: + raise error + + def _send_data(self, brty, data, addr): + data = (b"%s %s" % (brty.encode('latin'), hexlify(data))).strip() + log.log(logging.DEBUG-1, ">>> %s to %s:%s", data.decode(), *addr) + ret = self.socket.sendto(data, addr) + if ret != len(data): + raise nfc.clf.TransmissionError("failed to send data") + self.sent_data += len(data) + + def _recv_data(self, timeout, *brty_list): + time_to_return = None if timeout is None else (time.time() + timeout) + while timeout is None or time.time() < time_to_return: + wait = None if timeout is None else (time_to_return - time.time()) + if len(select.select([self.socket], [], [], wait)[0]) == 1: + data, addr = self.socket.recvfrom(1024) + log.log(logging.DEBUG-1, "<<< %s from %s:%d", data, *addr) + if data.startswith(b"RFOFF"): + raise nfc.clf.BrokenLinkError("RFOFF") + try: + brty, data = data.split() + except ValueError: + raise nfc.clf.TransmissionError("no data") + brty = brty.decode("ascii") + data = bytearray(unhexlify(data)) + self.rcvd_data += len(data) + if brty in brty_list: + return brty, data, addr + raise nfc.clf.TimeoutError("no data received") + + +def init(host, port): + import platform + device = Device(host, port) + device._vendor_name = platform.uname()[0] + device._device_name = "IP-Stack" + device._chipset_name = "UDP" + return device diff --git a/src/lib/nfc/dep.py b/src/lib/nfc/dep.py new file mode 100644 index 0000000..6d93bde --- /dev/null +++ b/src/lib/nfc/dep.py @@ -0,0 +1,895 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import src.lib.nfc.clf + +import os +import time +import collections +import struct +from binascii import hexlify + +import logging +log = logging.getLogger(__name__) + + +class DataExchangeProtocol(object): + class Counter(object): + def __init__(self): + self.sent = collections.defaultdict(int) + self.rcvd = collections.defaultdict(int) + + @property + def sent_count(self): + return sum(self.sent.values()) + + @property + def rcvd_count(self): + return sum(self.rcvd.values()) + + def __str__(self): + s = "sent/rcvd {0}/{1}".format(self.sent_count, self.rcvd_count) + for name in sorted(set(list(self.sent.keys()) + + list(self.rcvd.keys()))): + s += " {name} {sent}/{rcvd}".format( + name=name, sent=self.sent[name], rcvd=self.rcvd[name]) + return s + + def __init__(self, clf): + self.pcnt = DataExchangeProtocol.Counter() + self.clf = clf + self.gbi = b"" + self.gbt = b"" + + @property + def general_bytes(self): + """The general bytes received with the ATR exchange""" + pass + + @property + def role(self): + """Role in DEP communication, either 'Target' or 'Initiator'""" + pass + + +class Initiator(DataExchangeProtocol): + ROLE = "Initiator" + + def __init__(self, clf): + DataExchangeProtocol.__init__(self, clf) + self.target = None + self.miu = None # maximum information unit size + self.did = None # dep device identifier + self.nad = None # dep node address + self.gbt = None # general bytes from target + self.pni = None # dep packet number information + self.rwt = None # target response waiting time + self._acm = None # active communication mode flag + + @property + def role(self): + return "Initiator" + + @property + def general_bytes(self): + return self.gbt + + @property + def acm(self): + return bool(self._acm) + + def __str__(self): + msg = "NFC-DEP Initiator {brty} {mode} mode MIU={miu} RWT={rwt:.6f}" + return msg.format(brty=self.target.brty, miu=self.miu, rwt=self.rwt, + mode=("passive", "active")[self.acm]) + + def activate(self, target=None, **options): + """Activate DEP communication with a target.""" + log.debug("initiator options: {0}".format(options)) + + self.did = options.get('did', None) + self.nad = options.get('nad', None) + self.gbi = options.get('gbi', b'')[0:48] + self.brs = min(max(0, options.get('brs', 2)), 2) + self.lri = min(max(0, options.get('lri', 3)), 3) + if self._acm is None or 'acm' in options: + self._acm = bool(options.get('acm', True)) + + assert self.did is None or 0 <= self.did <= 255 + assert self.nad is None or 0 <= self.nad <= 255 + + ppi = (self.lri << 4) | (bool(self.gbi) << 1) | int(bool(self.nad)) + did = 0 if self.did is None else self.did + atr_req = ATR_REQ(os.urandom(10), did, 0, 0, ppi, self.gbi) + psl_req = PSL_REQ(did, (0, 9, 18)[self.brs], self.lri) + atr_res = psl_res = None + self.target = target + + if self.target is None and self.acm is True: + log.debug("searching active communication mode target at 106A") + tg = nfc.clf.RemoteTarget("106A", atr_req=atr_req.encode()) + try: + self.target = self.clf.sense(tg, iterations=2, interval=0.1) + except nfc.clf.UnsupportedTargetError: + self._acm = False + except nfc.clf.CommunicationError: + pass + else: + if self.target: + atr_res = ATR_RES.decode(self.target.atr_res) + else: + self._acm = None + + if self.target is None: + log.debug("searching passive communication mode target at 106A") + target = nfc.clf.RemoteTarget("106A") + target = self.clf.sense(target, iterations=2, interval=0.1) + if target and target.sel_res and bool(target.sel_res[0] & 0x40): + self.target = target + + if self.target is None and self.brs > 0: + log.debug("searching passive communication mode target at 212F") + target = nfc.clf.RemoteTarget("212F", sensf_req=b'\0\xFF\xFF\0\0') + target = self.clf.sense(target, iterations=2, interval=0.1) + if target and target.sensf_res.startswith(b'\1\1\xFE'): + atr_req.nfcid3 = target.sensf_res[1:9] + b'ST' + self.target = target + + if self.target and self.target.atr_res is None: + try: + atr_res = self.send_req_recv_res(atr_req, 1.0) + except nfc.clf.CommunicationError: + pass + if atr_res is None: + log.debug("NFC-DEP Attribute Request failed") + return None + + if self.target and atr_res: + if self.brs > ('106A', '212F', '424F').index(self.target.brty): + try: + psl_res = self.send_req_recv_res(psl_req, 0.1) + except nfc.clf.CommunicationError: + pass + if psl_res is None: + log.debug("NFC-DEP Parameter Selection failed") + return None + self.target.brty = ('212F', '424F')[self.brs-1] + + self.rwt = (4096/13.56E6 + * 2**(atr_res.wt if atr_res.wt < 15 else 14)) + self.miu = (atr_res.lr-3 - int(self.did is not None) + - int(self.nad is not None)) + self.gbt = atr_res.gb + self.pni = 0 + + log.info("running as " + str(self)) + return self.gbt + + def deactivate(self, release=True): + log.debug("deactivate {0}".format(self)) + req = RLS_REQ(self.did) if release else DSL_REQ(self.did) + try: + res = self.send_req_recv_res(req, 0.1) + except nfc.clf.CommunicationError: + return + else: + if res.did != req.did: + log.error("target returned wrong DID in " + res.PDU_NAME) + finally: + log.debug("packets {0}".format(self.pcnt)) + + def exchange(self, send_data, timeout): + def INF(pni, data, more, did, nad): + pdu_type = (DEP_REQ.LastInformation, DEP_REQ.MoreInformation)[more] + pfb = DEP_REQ.PFB(pdu_type, nad is not None, did is not None, pni) + return DEP_REQ(pfb, did, nad, data) + + def ACK(pni, did, nad): + pdu_type = DEP_REQ.PositiveAck + pfb = DEP_REQ.PFB(pdu_type, nad is not None, did is not None, pni) + return DEP_REQ(pfb, did, nad, data=None) + + def RTOX(rtox, did, nad): + if not 0 < rtox < 60: + error = "NFC-DEP RTOX must be in range 1 to 59" + raise nfc.clf.ProtocolError(error) + pdu_type = DEP_REQ.TimeoutExtension + pfb = DEP_REQ.PFB(pdu_type, nad is not None, did is not None, 0) + return DEP_REQ(pfb, did, nad, data=bytearray([rtox])) + + # log.debug("dep raw >> %s", hexlify(send_data).decode()) + send_data = bytearray(send_data) + + while send_data: + data = send_data[0:self.miu] + del send_data[0:self.miu] + req = INF(self.pni, data, bool(send_data), self.did, self.nad) + res = self.send_dep_req_recv_dep_res(req, self.rwt, timeout) + if res.pfb.fmt == DEP_RES.TimeoutExtension: + for i in range(3): + req = RTOX(res.data[0], self.did, self.nad) + rwt = res.data[0] * self.rwt + log.warning("target requested %.3f sec more time", rwt) + res = self.send_dep_req_recv_dep_res(req, rwt, timeout) + if res.pfb.fmt != DEP_RES.TimeoutExtension: + break + else: + log.error("too many timeout extension requests") + raise nfc.clf.TimeoutError("timeout extension") + if res.pfb.fmt == DEP_RES.PositiveAck: + if not send_data: + error = "unexpected or out-of-sequence NFC-DEP ACK PDU" + raise nfc.clf.ProtocolError(error) + if res.pfb.pni != self.pni: + raise nfc.clf.ProtocolError("wrong NFC-DEP packet number") + self.pni = (self.pni + 1) & 0x3 + + if ((res.pfb.fmt != DEP_RES.LastInformation and + res.pfb.fmt != DEP_RES.MoreInformation)): + error = "expected NFC-DEP INF PDU after sending" + raise nfc.clf.ProtocolError(error) + + recv_data = res.data + + while res.pfb.fmt == DEP_RES.MoreInformation: + req = ACK(self.pni, self.did, self.nad) + res = self.send_dep_req_recv_dep_res(req, self.rwt, timeout) + if res.pfb.fmt == DEP_RES.TimeoutExtension: + for i in range(3): + req = RTOX(res.data[0], self.did, self.nad) + rwt = res.data[0] * self.rwt + log.warning("target requested %.3f sec more time", rwt) + res = self.send_dep_req_recv_dep_res(req, rwt, timeout) + if res.pfb.fmt != DEP_RES.TimeoutExtension: + break + else: + log.error("too many timeout extension requests") + raise nfc.clf.TimeoutError("timeout extension") + if ((res.pfb.fmt != DEP_RES.LastInformation and + res.pfb.fmt != DEP_RES.MoreInformation)): + error = "NFC-DEP chaining not continued after ACK" + raise nfc.clf.ProtocolError(error) + if res.pfb.pni != self.pni: + raise nfc.clf.ProtocolError("wrong NFC-DEP packet number") + recv_data += res.data + self.pni = (self.pni + 1) & 0x3 + + # log.debug("dep raw << %s", hexlify(recv_data).decode()) + return recv_data + + def send_dep_req_recv_dep_res(self, req, rwt, timeout): + def NAK(pni, did, nad): + pdu_type = DEP_REQ.NegativeAck + pfb = DEP_REQ.PFB( + pdu_type, nad is not None, did is not None, self.pni) + return DEP_REQ(pfb, did, nad, data=None) + + def ATN(): + pdu_type = DEP_REQ.Attention + pfb = DEP_REQ.PFB(pdu_type, nad=False, did=False, pni=0) + return DEP_REQ(pfb, did=None, nad=None, data=None) + + def request_attention(self, n_retry_atn, rwt, deadline): + req = ATN() + for i in range(n_retry_atn): + timeout = min(rwt, deadline - time.time()) + if timeout <= 0: + raise nfc.clf.TimeoutError + try: + res = self.send_req_recv_res(req, timeout) + except nfc.clf.CommunicationError: + continue + if res.pfb.fmt == DEP_RES.TimeoutExtension: + error = "received NFC-DEP RTOX response to NACK or ATN" + raise nfc.clf.ProtocolError(error) + if res.pfb.fmt != DEP_RES.Attention: + error = "expected NFC-DEP Attention response" + raise nfc.clf.ProtocolError(error) + return + error = "unrecoverable NFC-DEP error in attention request" + raise nfc.clf.ProtocolError(error) + + def request_retransmission(self, n_retry_nak, rwt, deadline): + req = NAK(self.pni, self.did, self.nad) + for i in range(n_retry_nak): + timeout = min(rwt, deadline - time.time()) + if timeout <= 0: + raise nfc.clf.TimeoutError + try: + res = self.send_req_recv_res(req, timeout) + except nfc.clf.CommunicationError: + continue + if res.pfb.fmt == DEP_RES.TimeoutExtension: + error = "received NFC-DEP RTOX response to NACK or ATN" + raise nfc.clf.ProtocolError(error) + expected = (DEP_RES.LastInformation, DEP_RES.MoreInformation) + if res.pfb.fmt not in expected: + error = "unrecoverable NFC-DEP transmission error" + raise nfc.clf.ProtocolError(error) + return res + error = "unrecoverable NFC-DEP error in retransmission request" + raise nfc.clf.ProtocolError(error) + + if rwt > timeout: + text = "response waiting time %.3f exceeds the timeout of %.3f sec" + log.warning(text, rwt, timeout) + + deadline = time.time() + timeout + while True: + timeout = min(rwt, deadline - time.time()) + if timeout <= 0: + raise nfc.clf.TimeoutError() + try: + res = self.send_req_recv_res(req, timeout) + break + except nfc.clf.TimeoutError: + request_attention(self, 2, rwt, deadline) + continue + except nfc.clf.TransmissionError: + res = request_retransmission(self, 2, rwt, deadline) + break + + if res.pfb.fmt == DEP_RES.NegativeAck: + error = "received NFC-DEP NACK PDU from Target" + raise nfc.clf.ProtocolError(error) + + return res + + def send_req_recv_res(self, req, timeout): + log.debug(">> {0}".format(req)) + pcnt_key = req.PDU_NAME[:3] + if isinstance(req, DEP_REQ): + pcnt_key += " " + req.pfb.FMT_NAME + self.pcnt.sent[pcnt_key] += 1 + + cmd = self.encode_frame(req) + rsp = self.clf.exchange(cmd, timeout) + res = self.decode_frame(rsp) + if res.PDU_NAME[0:3] != req.PDU_NAME[0:3]: + raise nfc.clf.ProtocolError("invalid response for " + req.PDU_NAME) + + log.debug("<< {0}".format(res)) + pcnt_key = res.PDU_NAME[:3] + if isinstance(res, DEP_RES): + pcnt_key += " " + res.pfb.FMT_NAME + self.pcnt.rcvd[pcnt_key] += 1 + return res + + def encode_frame(self, packet): + frame = packet.encode() + frame = struct.pack("B", len(frame) + 1) + frame + if self.target.brty == '106A': + frame = b'\xF0' + frame + return bytearray(frame) + + def decode_frame(self, frame): + if self.target.brty == '106A' and frame.pop(0) != 0xF0: + error = "first NFC-DEP frame byte must be F0h for 106A" + raise nfc.clf.ProtocolError(error) + if len(frame) != frame.pop(0): + error = "NFC-DEP frame length byte must be data length + 1" + raise nfc.clf.ProtocolError(error) + if len(frame) < 2: + error = "NFC-DEP frame length byte must be from 3 to 255" + raise nfc.clf.TransmissionError(error) + if frame[0] != 0xD5 or frame[1] not in (1, 5, 7, 9, 11): + raise nfc.clf.ProtocolError("invalid NFC-DEP response code") + res_name = {1: 'ATR', 5: 'PSL', 7: 'DEP', 9: 'DSL', 11: 'RLS'} + return eval(res_name[frame[1]] + "_RES").decode(frame) + + +class Target(DataExchangeProtocol): + def __init__(self, clf): + DataExchangeProtocol.__init__(self, clf) + self.miu = None # maximum information unit size + self.did = None # dep device identifier + self.nad = None # dep node address + self.gbi = None # general bytes from initiator + self.pni = None # dep packet number information + self.rwt = None # target response waiting time + + @property + def role(self): + return "Target" + + @property + def general_bytes(self): + return self.gbi + + def __str__(self): + msg = "NFC-DEP Target {brty} {mode} mode MIU={miu} RWT={rwt:.6f}" + return msg.format(brty=self.target.brty, miu=self.miu, rwt=self.rwt, + mode=("passive", "active")[self.acm]) + + def activate(self, timeout=None, **options): + """Activate DEP communication as a target.""" + + if timeout is None: + timeout = 1.0 + gbt = options.get('gbt', b'')[0:47] + lrt = min(max(0, options.get('lrt', 3)), 3) + rwt = min(max(0, options.get('rwt', 8)), 14) + + pp = (lrt << 4) | (bool(gbt) << 1) | int(bool(self.nad)) + nfcid3t = bytearray.fromhex("01FE") + os.urandom(6) + b"ST" + atr_res = ATR_RES(nfcid3t, 0, 0, 0, rwt, pp, gbt) + atr_res = atr_res.encode() + + target = nfc.clf.LocalTarget(atr_res=atr_res) + target.sens_res = bytearray.fromhex("0101") + target.sdd_res = bytearray.fromhex("08") + os.urandom(3) + target.sel_res = bytearray.fromhex("40") + target.sensf_res = bytearray.fromhex("01") + nfcid3t[0:8] + target.sensf_res += bytearray.fromhex("00000000 00000000 FFFF") + + target = self.clf.listen(target, timeout) + + if target and target.atr_req and target.dep_req: + log.debug("activated as " + str(target)) + + atr_req = ATR_REQ.decode(target.atr_req) + self.lrt = lrt + self.gbt = gbt + self.gbi = atr_req.gb + self.miu = atr_req.lr - 3 + self.rwt = 4096/13.56E6 * pow(2, rwt) + self.did = atr_req.did if atr_req.did > 0 else None + self.acm = not (target.sens_res or target.sensf_res) + self.cmd = bytearray( + struct.pack("B", len(target.dep_req)+1) + target.dep_req) + if target.brty == "106A": + self.cmd = bytearray(b"\xF0" + self.cmd) + self.target = target + + self.pcnt.rcvd["ATR"] += 1 + self.pcnt.sent["ATR"] += 1 + log.info("running as " + str(self)) + + return self.gbi + + def deactivate(self, data=bytearray()): + try: + log.debug("deactivate {0}".format(self)) + self._deactivate(data) + finally: + log.debug("packets {0}".format(self.pcnt)) + + def _deactivate(self, data): + def INF(pni, data, did, nad): + pdu_type = DEP_RES.LastInformation + pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, pni) + return DEP_RES(pfb, did, nad, data) + + def ATN(did, nad): + pdu_type = DEP_RES.Attention + pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, 0) + return DEP_RES(pfb, did, nad, data=None) + + res = None + deadline = time.time() + 1.0 + while time.time() < deadline: # pragma: no branch + try: + req = self.send_res_recv_req(res, deadline) + except nfc.clf.CommunicationError: + return + if req is None: + return + if req.did == self.did: + if type(req) in (DSL_REQ, RLS_REQ): + RES = DSL_RES if type(req) == DSL_REQ else RLS_RES + try: + self.send_res_recv_req(RES(self.did), 0) + except nfc.clf.CommunicationError: + pass + return + if type(req) == DEP_REQ: + if req.pfb.fmt == DEP_REQ.Attention: + res = ATN(self.did, self.nad) + else: + res = INF(req.pfb.pni, data, self.did, self.nad) + continue + res = None + + def exchange(self, send_data, timeout): + def INF(pni, data, more, did, nad): + pdu_type = (DEP_RES.LastInformation, DEP_RES.MoreInformation)[more] + pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, pni) + return DEP_RES(pfb, did, nad, data) + + def ACK(pni, did, nad): + pdu_type = DEP_RES.PositiveAck + pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, pni) + return DEP_RES(pfb, did, nad, data=None) + + if send_data is not None and len(send_data) == 0: + raise ValueError("send_data must not be empty") + + deadline = time.time() + timeout + + if self.cmd is not None: + # first command frame received in activate is injected in + # send_res_recv_req and self.cmd then set to None + assert send_data is None, "send_data should be None on first call" + req = self.send_dep_res_recv_dep_req(None, deadline) + self.pni = 0 + else: + send_data = bytearray(send_data) + while send_data: + data = send_data[0:self.miu] + more = len(send_data) > self.miu + res = INF(self.pni, data, more, self.did, self.nad) + req = self.send_dep_res_recv_dep_req(res, deadline) + if req is None: + return None + if more: + if req.pfb.fmt is not DEP_REQ.PositiveAck: + error = "expected ACK in NFC-DEP chaining" + raise nfc.clf.ProtocolError(error) + self.pni = (self.pni + 1) & 0x3 + if req.pfb.pni != self.pni: + raise nfc.clf.ProtocolError("wrong NFC-DEP packet number") + del send_data[0:self.miu] + + recv_data = bytearray() + while req.pfb.fmt == DEP_REQ.MoreInformation: + recv_data += req.data + res = ACK(self.pni, self.did, self.nad) + req = self.send_dep_res_recv_dep_req(res, deadline) + if req is None: + return None + self.pni = (self.pni + 1) & 0x3 + if req.pfb.pni != self.pni: + raise nfc.clf.ProtocolError("wrong NFC-DEP packet number") + + recv_data += req.data + return recv_data + + def send_timeout_extension(self, rtox): + def RTOX(rtox, did, nad): + pdu_type = DEP_RES.TimeoutExtension + pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, 0) + return DEP_RES(pfb, did, nad, data=bytearray([rtox])) + + res = RTOX(rtox, self.did, self.nad) + req = self.send_dep_res_recv_dep_req(res, deadline=time.time()+1) + if type(req) == DEP_REQ and req.pfb.fmt == DEP_REQ.TimeoutExtension: + return req.data[0] & 0x3F + + def send_dep_res_recv_dep_req(self, dep_res, deadline): + def ATN(did, nad): + pdu_type = DEP_RES.Attention + pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, 0) + return DEP_RES(pfb, did, nad, data=None) + + res = dep_res + dep_req = None + while dep_req is None: + req = self.send_res_recv_req(res, deadline) + if req is None: + return None + elif req.did != self.did: + log.debug("ignore non-matching device identifier") + res = None + elif type(req) == DSL_REQ: + return self.send_res_recv_req(DSL_RES(self.did), 0) + elif type(req) == RLS_REQ: + return self.send_res_recv_req(RLS_RES(self.did), 0) + elif type(req) == DEP_REQ: + if req.pfb.fmt == DEP_REQ.Attention: + res = ATN(self.did, self.nad) + elif req.pfb.fmt == DEP_REQ.NegativeAck: + res = dep_res + elif req.pfb.fmt == DEP_REQ.TimeoutExtension: + dep_req = req + elif req.pfb.pni == self.pni: + res = dep_res + else: + dep_req = req + else: + log.debug("invalid command in data exchange context") + res = None + return dep_req + + def send_res_recv_req(self, res, deadline): + frame = None + + if self.cmd is not None: + # first command is received in activate + frame, self.cmd = self.cmd, None + else: + if res is not None: + log.debug(">> {0}".format(res)) + pcnt_key = res.PDU_NAME[:3] + if isinstance(res, DEP_RES): + pcnt_key += " " + res.pfb.FMT_NAME + self.pcnt.sent[pcnt_key] += 1 + frame = self.encode_frame(res) + while True: + timeout = deadline-time.time() if deadline > time.time() else 0 + try: + frame = self.clf.exchange(frame, timeout=timeout) + except nfc.clf.TransmissionError: + frame = None + else: + break + + if frame: + req = self.decode_frame(frame) + log.debug("<< {0}".format(req)) + pcnt_key = req.PDU_NAME[:3] + if isinstance(req, DEP_REQ): + pcnt_key += " " + req.pfb.FMT_NAME + self.pcnt.rcvd[pcnt_key] += 1 + return req + + def encode_frame(self, packet): + frame = packet.encode() + frame = struct.pack("B", len(frame) + 1) + frame + if self.target.brty == '106A': + frame = b'\xF0' + frame + return bytearray(frame) + + def decode_frame(self, frame): + if self.target.brty == '106A' and frame.pop(0) != 0xF0: + error = "first NFC-DEP frame byte must be F0h for 106A" + raise nfc.clf.ProtocolError(error) + if len(frame) != frame.pop(0): + error = "NFC-DEP frame length byte must be data length + 1" + raise nfc.clf.ProtocolError(error) + if len(frame) < 2: + error = "NFC-DEP frame length byte must be from 3 to 255" + raise nfc.clf.TransmissionError(error) + if frame[0] != 0xD4 or frame[1] not in (0, 4, 6, 8, 10): + raise nfc.clf.ProtocolError("invalid NFC-DEP command code") + req_name = {0: 'ATR', 4: 'PSL', 6: 'DEP', 8: 'DSL', 10: 'RLS'} + return eval(req_name[frame[1]] + "_REQ").decode(frame) + + +# +# Data Exchange Protocol Data Units +# +class ATR_REQ_RES(object): + def __str__(self): + nfcid3, gb = [hexlify(ba).decode() for ba in [self.nfcid3, self.gb]] + return self.PDU_SHOW.format(self=self, nfcid3=nfcid3, gb=gb) + + @property + def lr(self): + return (64, 128, 192, 254)[(self.pp >> 4) & 0x3] + + +class ATR_REQ(ATR_REQ_RES): + PDU_CODE = bytearray(b'\xD4\x00') + PDU_NAME = 'ATR-REQ' + PDU_SHOW = "{self.PDU_NAME} NFCID3={nfcid3} DID={self.did:02x} "\ + "BS={self.bs:02x} BR={self.br:02x} PP={self.pp:02x} GB={gb}" + + def __init__(self, nfcid3, did, bs, br, pp, gb): + self.nfcid3, self.did, self.bs, self.br, self.pp, self.gb = \ + nfcid3, did, bs, br, pp, gb + + def __len__(self): + return 16 + len(self.gb) + + @staticmethod + def decode(data): + if data.startswith(ATR_REQ.PDU_CODE): + nfcid3, (did, bs, br, pp) = data[2:12], data[12:16] + gb = data[16:] if pp & 0x02 else bytearray() + return ATR_REQ(nfcid3, did, bs, br, pp, gb) + + def encode(self): + data = ATR_REQ.PDU_CODE + self.nfcid3 + data.extend([self.did, self.bs, self.br, self.pp]) + return data + self.gb + + +class ATR_RES(ATR_REQ_RES): + PDU_CODE = bytearray(b'\xD5\x01') + PDU_NAME = 'ATR-RES' + PDU_SHOW = "{self.PDU_NAME} NFCID3={nfcid3} DID={self.did:02x} "\ + "BS={self.bs:02x} BR={self.br:02x} TO={self.to:02x} "\ + "PP={self.pp:02x} GB={gb}" + + def __init__(self, nfcid3, did, bs, br, to, pp, gb): + self.nfcid3, self.did, self.bs, self.br, self.to, self.pp, self.gb = \ + nfcid3, did, bs, br, to, pp, gb + + def __len__(self): + return 17 + len(self.gb) + + @staticmethod + def decode(data): + if data.startswith(ATR_RES.PDU_CODE): + nfcid3, (did, bs, br, to, pp) = data[2:12], data[12:17] + gb = data[17:] if pp & 0x02 else bytearray() + return ATR_RES(nfcid3, did, bs, br, to, pp, gb) + + def encode(self): + data = ATR_RES.PDU_CODE + self.nfcid3 + data.extend([self.did, self.bs, self.br, self.to, self.pp]) + return data + self.gb + + @property + def wt(self): + return self.to & 0x0F + + +class PSL_REQ_RES(object): + def __str__(self): + return self.PDU_SHOW.format(name=self.PDU_NAME, self=self) + + @classmethod + def decode(cls, data): + if data.startswith(cls.PDU_CODE): + try: + return cls(*data[2:]) + except TypeError: + errstr = "invalid format of the " + cls.PDU_NAME + raise nfc.clf.ProtocolError(errstr) + + +class PSL_REQ(PSL_REQ_RES): + PDU_CODE = bytearray(b'\xD4\x04') + PDU_NAME = 'PSL-REQ' + PDU_SHOW = "{name} DID={self.did:02x} BRS={self.brs:02x} " \ + "FSL={self.fsl:02x}" + + def __init__(self, did, brs, fsl): + self.did, self.brs, self.fsl = did if did else 0, brs, fsl + + def encode(self): + return PSL_REQ.PDU_CODE + bytearray([self.did, self.brs, self.fsl]) + + @property + def dsi(self): + return self.brs >> 3 & 0x07 + + @property + def dri(self): + return self.brs & 0x07 + + @property + def lr(self): + return (64, 128, 192, 254)[self.fsl & 0x03] + + +class PSL_RES(PSL_REQ_RES): + PDU_CODE = bytearray(b'\xD5\x05') + PDU_NAME = 'PSL-RES' + PDU_SHOW = "{name} DID={self.did:02x}" + + def __init__(self, did): + self.did = did + + def encode(self): + return PSL_RES.PDU_CODE + bytearray([self.did]) + + +class DEP_REQ_RES(object): + PDU_SHOW = "{self.PDU_NAME} {self.pfb.FMT_NAME} PNI={self.pfb.pni} "\ + "DID={self.did} NAD={self.nad} DATA={data}" + + class PFB: + def __init__(self, fmt, nad, did, pni): + self.fmt, self.nad, self.did, self.pni = fmt, nad, did, pni + + @property + def FMT_NAME(self): + return {0: "INF", 1: "I++", 4: "ACK", 5: "NAK", 8: "ATN", + 9: "TOX"}.get(self.fmt, "{0:04b}".format(self.fmt)) + + @property + def type(self): return self.fmt + + LastInformation, MoreInformation, PositiveAck, NegativeAck,\ + Attention, TimeoutExtension = (0, 1, 4, 5, 8, 9) + + def __init__(self, pfb, did, nad, data): + self.pfb, self.did, self.nad = pfb, did, nad + self.data = bytearray() if data is None else data + + def __str__(self): + data = hexlify(self.data).decode() + return self.PDU_SHOW.format(self=self, data=data) + + def bytes(self): + data = hexlify(self.data) + return self.PDU_SHOW.format(self=self, data=data) + + @classmethod + def decode(cls, data): + if data.startswith(cls.PDU_CODE): + del data[0:2] + try: + pfb = data.pop(0) + pfb = cls.PFB(pfb >> 4, bool(pfb & 8), bool(pfb & 4), pfb & 3) + did = data.pop(0) if pfb.did else None + nad = data.pop(0) if pfb.nad else None + except IndexError: + errstr = "invalid format of the " + cls.PDU_NAME + raise nfc.clf.ProtocolError(errstr) + return cls(pfb, did, nad, data) + + def encode(self): + pfb = self.pfb + pfb = (pfb.fmt << 4) | (pfb.nad << 3) | (pfb.did << 2) | (pfb.pni) + data = self.PDU_CODE + struct.pack("B", pfb) + if self.pfb.did: + data.append(self.did) + if self.pfb.nad: + data.append(self.nad) + return data + self.data + + +class DEP_REQ(DEP_REQ_RES): + PDU_CODE = bytearray(b'\xD4\x06') + PDU_NAME = 'DEP-REQ' + + +class DEP_RES(DEP_REQ_RES): + PDU_CODE = bytearray(b'\xD5\x07') + PDU_NAME = 'DEP-RES' + + +class DSL_REQ_RES(object): + def __init__(self, did): + self.did = did + + def __str__(self): + return "{0} DID={1}".format(self.PDU_NAME, self.did) + + @classmethod + def decode(cls, data): + if data.startswith(cls.PDU_CODE): + if len(data) > 3: + errstr = "invalid format of the " + cls.PDU_NAME + raise nfc.clf.ProtocolError(errstr) + return cls(data[2] if len(data) == 3 else None) + + def encode(self): + return self.PDU_CODE + (b"" + if self.did is None + else struct.pack("B", self.did)) + + +class DSL_REQ(DSL_REQ_RES): + PDU_CODE = bytearray(b'\xD4\x08') + PDU_NAME = 'DSL-REQ' + + +class DSL_RES(DSL_REQ_RES): + PDU_CODE = bytearray(b'\xD5\x09') + PDU_NAME = 'DSL-RES' + + +class RLS_REQ_RES(DSL_REQ_RES): + pass + + +class RLS_REQ(RLS_REQ_RES): + PDU_CODE = bytearray(b'\xD4\x0A') + PDU_NAME = 'RLS-REQ' + + +class RLS_RES(RLS_REQ_RES): + PDU_CODE = bytearray(b'\xD5\x0B') + PDU_NAME = 'RLS-RES' diff --git a/src/lib/nfc/handover/__init__.py b/src/lib/nfc/handover/__init__.py new file mode 100644 index 0000000..e887b4d --- /dev/null +++ b/src/lib/nfc/handover/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2012 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +""" +The nfc.handover module implements the NFC Forum Connection Handover +1.2 protocol as a server and client class that simplify realization of +handover selector and requester functionality. + +""" +from src.lib.nfc.handover.server import HandoverServer # noqa: F401 +from src.lib.nfc.handover.client import HandoverClient # noqa: F401 diff --git a/src/lib/nfc/handover/client.py b/src/lib/nfc/handover/client.py new file mode 100644 index 0000000..2715e76 --- /dev/null +++ b/src/lib/nfc/handover/client.py @@ -0,0 +1,118 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +# +# Negotiated Connection Handover - Client Base Class +# +import binascii +import logging +import time +import ndef +import src.lib.nfc + + +log = logging.getLogger(__name__) + + +class HandoverClient(object): + """ NFC Forum Connection Handover client + """ + def __init__(self, llc): + self.socket = None + self.llc = llc + + def connect(self, recv_miu=248, recv_buf=2): + """Connect to the remote handover server if available. Raises + :exc:`nfc.llcp.ConnectRefused` if the remote device does not + have a handover service or the service does not accept any + more connections.""" + socket = nfc.llcp.Socket(self.llc, nfc.llcp.DATA_LINK_CONNECTION) + socket.setsockopt(nfc.llcp.SO_RCVBUF, recv_buf) + socket.setsockopt(nfc.llcp.SO_RCVMIU, recv_miu) + socket.connect("urn:nfc:sn:handover") + server = socket.getpeername() + log.debug("handover client connected to remote sap {0}".format(server)) + self.socket = socket + + def close(self): + """Disconnect from the remote handover server.""" + if self.socket: + self.socket.close() + self.socket = None + + def send_records(self, records): + """Send handover request message records to the remote server.""" + log.debug("sending '{0}' message".format(records[0].type)) + try: + octets = b''.join(ndef.message_encoder(records)) + except ndef.EncodeError as error: + log.error(repr(error)) + else: + return self.send_octets(octets) + + def send_octets(self, octets): + log.debug(">>> %s", binascii.hexlify(octets).decode()) + miu = self.socket.getsockopt(nfc.llcp.SO_SNDMIU) + while len(octets) > 0: + if self.socket.send(octets[0:miu]): + octets = octets[miu:] + else: + break + return len(octets) == 0 + + def recv_records(self, timeout=None): + """Receive a handover select message from the remote server.""" + octets = self.recv_octets(timeout) + records = list(ndef.message_decoder(octets, 'relax')) if octets else [] + if records and records[0].type == "urn:nfc:wkt:Hs": + log.debug("received '{0}' message".format(records[0].type)) + return list(ndef.message_decoder(octets, 'relax')) + else: + log.error("received invalid message %s", binascii.hexlify(octets)) + return [] + + def recv_octets(self, timeout=None): + octets = bytearray() + started = time.time() + while self.socket.poll("recv", timeout): + try: + octets += self.socket.recv() + except TypeError: + log.debug("data link connection closed") + return b'' # recv() returned None + try: + list(ndef.message_decoder(octets, 'strict', {})) + log.debug("<<< %s", binascii.hexlify(octets).decode()) + return bytes(octets) + except ndef.DecodeError: + log.debug("message is incomplete (%d byte)", len(octets)) + if timeout: + timeout -= time.time() - started + started = time.time() + log.debug("%.3f seconds left to timeout", timeout) + continue # incomplete message + + def __enter__(self): + self.connect() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() diff --git a/src/lib/nfc/handover/server.py b/src/lib/nfc/handover/server.py new file mode 100644 index 0000000..3817c34 --- /dev/null +++ b/src/lib/nfc/handover/server.py @@ -0,0 +1,128 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +# +# Negotiated Connection Handover - Server Base Class +# +import threading +import binascii +import logging +import errno +import ndef +import src.lib.nfc + + +log = logging.getLogger(__name__) + + +class HandoverServer(threading.Thread): + """ NFC Forum Connection Handover server + """ + def __init__(self, llc, request_size_limit=0x10000, + recv_miu=1984, recv_buf=15): + socket = nfc.llcp.Socket(llc, nfc.llcp.DATA_LINK_CONNECTION) + recv_miu = socket.setsockopt(nfc.llcp.SO_RCVMIU, recv_miu) + recv_buf = socket.setsockopt(nfc.llcp.SO_RCVBUF, recv_buf) + socket.bind('urn:nfc:sn:handover') + log.info("handover server bound to port {0} (MIU={1}, RW={2})" + .format(socket.getsockname(), recv_miu, recv_buf)) + socket.listen(backlog=2) + threading.Thread.__init__(self, name='urn:nfc:sn:handover', + target=self.listen, args=(llc, socket)) + + def listen(self, llc, socket): + log.debug("handover listen thread started") + try: + while True: + client_socket = socket.accept() + client_thread = threading.Thread(target=self.serve, + args=(client_socket,)) + client_thread.start() + except nfc.llcp.Error as error: + (log.debug if error.errno == errno.EPIPE else log.error)(error) + finally: + socket.close() + log.debug("handover listen thread terminated") + + def serve(self, socket): + peer_sap = socket.getpeername() + log.info("serving handover client on remote sap {0}".format(peer_sap)) + send_miu = socket.getsockopt(nfc.llcp.SO_SNDMIU) + try: + while socket.poll("recv"): + request = bytearray() + while socket.poll("recv"): + request += socket.recv() + + if len(request) == 0: + continue # need some data + + try: + list(ndef.message_decoder(request, 'strict', {})) + except ndef.DecodeError: + continue # need more data + + response = self._process_request_data(request) + + for offset in range(0, len(response), send_miu): + fragment = response[offset:offset + send_miu] + if not socket.send(fragment): + return # connection closed + + except nfc.llcp.Error as error: + (log.debug if error.errno == errno.EPIPE else log.error)(error) + finally: + socket.close() + log.debug("handover serve thread terminated") + + def _process_request_data(self, octets): + log.debug("<<< %s", binascii.hexlify(octets).decode()) + try: + records = list(ndef.message_decoder(octets, 'relax')) + except ndef.DecodeError as error: + log.error(repr(error)) + return b'' + + if records[0].type == 'urn:nfc:wkt:Hr': + records = self.process_handover_request_message(records) + else: + log.error("received unknown request message") + records = [] + + octets = b''.join(ndef.message_encoder(records)) + log.debug(">>> %s", binascii.hexlify(octets).decode()) + return octets + + def process_handover_request_message(self, records): + """Process a handover request message. The *records* argument holds a + list of :class:`ndef.Record` objects decoded from the received + handover request message octets, where the first record type is + ``urn:nfc:wkt:Hr``. The method returns a list of :class:`ndef.Record` + objects with the first record typ ``urn:nfc:wkt:Hs``. + + This method should be overwritten by a subclass to customize + it's behavior. The default implementation returns a + :class:`ndef.HandoverSelectRecord` with version ``1.2`` and no + alternative carriers. + + """ + log.warning("default process_request method should be overwritten") + return [ndef.HandoverSelectRecord('1.2')] diff --git a/src/lib/nfc/llcp/__init__.py b/src/lib/nfc/llcp/__init__.py new file mode 100644 index 0000000..74631ac --- /dev/null +++ b/src/lib/nfc/llcp/__init__.py @@ -0,0 +1,38 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +""" +The nfc.llcp module implements the NFC Forum Logical Link Control +Protocol (LLCP) specification and provides a socket interface to use +the connection-less and connection-mode transport facilities of LLCP. +""" +from .socket import Socket # noqa: F401 +from .llc import LOGICAL_DATA_LINK, DATA_LINK_CONNECTION # noqa: F401 +from .err import Error, ConnectRefused, errno # noqa: F401 + +SO_SNDMIU = 1 +SO_RCVMIU = 2 +SO_SNDBUF = 3 +SO_RCVBUF = 4 +SO_SNDBSY = 5 +SO_RCVBSY = 6 + +MSG_DONTWAIT = 0b00000001 diff --git a/src/lib/nfc/llcp/err.py b/src/lib/nfc/llcp/err.py new file mode 100644 index 0000000..1a0a498 --- /dev/null +++ b/src/lib/nfc/llcp/err.py @@ -0,0 +1,42 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import os +import errno + + +class Error(IOError): + def __init__(self, errno): + super(Error, self).__init__(errno, os.strerror(errno)) + + def __str__(self): + return "nfc.llcp.Error: [{0}] {1}".format( + errno.errorcode[self.errno], self.strerror) + + +class ConnectRefused(Error): + def __init__(self, reason): + super(ConnectRefused, self).__init__(errno.ECONNREFUSED) + self.reason = reason + + def __str__(self): + return "nfc.llcp.ConnectRefused: [{0}] {1} with reason {2}".format( + errno.errorcode[self.errno], self.strerror, self.reason) diff --git a/src/lib/nfc/llcp/llc.py b/src/lib/nfc/llcp/llc.py new file mode 100644 index 0000000..d92fcbc --- /dev/null +++ b/src/lib/nfc/llcp/llc.py @@ -0,0 +1,886 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +from . import tco +from . import pdu +from . import err +from . import sec +import src.lib.nfc.llcp +import src.lib.nfc.clf +import src.lib.nfc.dep + +import re +import time +import errno +import random +import threading +import collections + +import logging +log = logging.getLogger(__name__) + +RAW_ACCESS_POINT, LOGICAL_DATA_LINK, DATA_LINK_CONNECTION = range(3) + +wks_map = { + b"urn:nfc:sn:sdp": 1, + b"urn:nfc:sn:snep": 4, +} + +service_name_format = \ + re.compile(b"^urn:nfc:[x]?sn:[a-zA-Z][a-zA-Z0-9-_:\\.]*$") + + +class ServiceAccessPoint(object): + def __init__(self, addr, llc): + self.llc = llc + self.addr = addr + self.sock_list = collections.deque() + self.send_list = collections.deque() + + def __str__(self): + return "SAP {0:>2}".format(self.addr) + + @property + def mode(self): + with self.llc.lock: + try: + if isinstance(self.sock_list[0], tco.RawAccessPoint): + return RAW_ACCESS_POINT + if isinstance(self.sock_list[0], tco.LogicalDataLink): + return LOGICAL_DATA_LINK + if isinstance(self.sock_list[0], tco.DataLinkConnection): + return DATA_LINK_CONNECTION + except IndexError: + return 0 + + def insert_socket(self, socket): + with self.llc.lock: + try: + insertable = isinstance(socket, type(self.sock_list[0])) + except IndexError: + insertable = True + if insertable: + socket.bind(self.addr) + self.sock_list.appendleft(socket) + else: + log.error("can't insert socket of different type") + return insertable + + def remove_socket(self, socket): + assert socket.addr == self.addr + socket.close() + with self.llc.lock: + try: + self.sock_list.remove(socket) + except ValueError: + pass + if len(self.sock_list) == 0: + # completely remove this sap + self.llc.sap[self.addr] = None + + def send(self, send_pdu): + self.send_list.append(send_pdu) + + def shutdown(self): + while True: + try: + socket = self.sock_list.pop() + except IndexError: + return + log.debug("shutdown socket %s" % str(socket)) + socket.bind(None) + socket.close() + + # + # enqueue() and dequeue() are called from llc run thread + # + def enqueue(self, rcvd_pdu): + with self.llc.lock: + if isinstance(rcvd_pdu, pdu.Connect): + for socket in self.sock_list: + if socket.state.LISTEN: + socket.enqueue(rcvd_pdu) + break + else: + args = (rcvd_pdu.ssap, rcvd_pdu.dsap, 0x02) + self.send(pdu.DisconnectedMode(*args)) + else: + for socket in self.sock_list: + if rcvd_pdu.ssap == socket.peer or socket.peer is None: + socket.enqueue(rcvd_pdu) + break + else: + if rcvd_pdu.name in tco.DataLinkConnection.DLC_PDU_NAMES: + args = (rcvd_pdu.ssap, rcvd_pdu.dsap, 0x01) + self.send(pdu.DisconnectedMode(*args)) + else: + log.debug("%s discard PDU %s", self, rcvd_pdu) + + def dequeue(self, miu_size, icv_size): + with self.llc.lock: + for socket in self.sock_list: + send_pdu = socket.dequeue(miu_size, icv_size) + if send_pdu: + return send_pdu + else: + try: + return self.send_list.popleft() + except IndexError: + pass + + def sendack(self): + with self.llc.lock: + for socket in self.sock_list: + send_pdu = socket.sendack() + if send_pdu: + return send_pdu + + +class ServiceDiscovery(object): + def __init__(self, llc): + self.llc = llc + self.snl = dict() + self.tids = list(range(256)) + self.resp = threading.Condition(self.llc.lock) + self.sent = dict() + self.sdreq = collections.deque() + self.sdres = collections.deque() + self.dmpdu = collections.deque() + + def __str__(self): + return "SAP 1" + + @property + def mode(self): + return LOGICAL_DATA_LINK + + def resolve(self, name): + with self.resp: + if self.snl is None: + return None + log.debug("resolve service name %r", name) + try: + return self.snl[name] + except KeyError: + pass + tid = random.choice(self.tids) + self.tids.remove(tid) + self.sdreq.append((tid, name)) + while self.snl is not None and name not in self.snl: + self.resp.wait() + return None if self.snl is None else self.snl[name] + + # + # enqueue() and dequeue() are called from llc run thread + # + def enqueue(self, rcvd_pdu): + with self.llc.lock: + if ((isinstance(rcvd_pdu, pdu.ServiceNameLookup) + and self.snl is not None)): + + for tid, sap in rcvd_pdu.sdres: + try: + name = self.sent[tid] + except KeyError: + continue + log.debug("resolved %r to remote addr %d", name, sap) + csn, sap = sap >> 6 & 1, sap & 63 + if csn: + sap = 1 + self.snl[name] = sap + self.tids.append(tid) + self.resp.notify_all() + + for tid, name in rcvd_pdu.sdreq: + try: + sap = self.llc.snl[name] + except KeyError: + sap = 0 + self.sdres.append((tid, sap)) + + def dequeue(self, miu_size, icv_size): + with self.llc.lock: + if len(self.sdres) > 0 or len(self.sdreq) > 0: + send_pdu = pdu.ServiceNameLookup(dsap=1, ssap=1) + # add service discovery responses + while miu_size > 0: + try: + send_pdu.sdres.append(self.sdres.popleft()) + miu_size -= 4 + except IndexError: + break + # add service discovery requests + for i in range(len(self.sdreq)): + tid, name = self.sdreq[0] + if 3 + len(name) > miu_size: + self.sdreq.rotate(-1) + else: + send_pdu.sdreq.append(self.sdreq.popleft()) + self.sent[tid] = name + miu_size -= 3 + len(name) + return send_pdu + if len(self.dmpdu) > 0 and miu_size > 0: + return self.dmpdu.popleft() + + def shutdown(self): + with self.llc.lock: + self.snl = None + self.resp.notify_all() + + +class LogicalLinkController(object): + class LinkState(object): + def __init__(self): + self.names = ("SHUTDOWN", "LISTEN", "CONNECT", "CONNECTED", + "ESTABLISHED", "DISCONNECT", "CLOSED") + self.value = self.names.index("SHUTDOWN") + + def __str__(self): + return self.names[self.value] + + def __getattr__(self, name): + return self.value == self.names.index(name) + + def __setattr__(self, name, value): + if name not in ("names", "value"): + value, name = self.names.index(name), "value" + parent = super(LogicalLinkController.LinkState, self) + parent.__setattr__(name, value) + + class Counter(object): + def __init__(self): + self.sent = collections.defaultdict(int) + self.rcvd = collections.defaultdict(int) + + @property + def sent_count(self): + return sum(self.sent.values()) + + @property + def rcvd_count(self): + return sum(self.rcvd.values()) + + def __str__(self): + s = "sent/rcvd {0}/{1}".format(self.sent_count, self.rcvd_count) + for name in sorted(set(list(self.sent.keys()) + + list(self.rcvd.keys()))): + s += " {name} {sent}/{rcvd}".format( + name=name, sent=self.sent[name], rcvd=self.rcvd[name]) + return s + + def __init__(self, **options): + self.pcnt = LogicalLinkController.Counter() + self.link = LogicalLinkController.LinkState() + self.lock = threading.RLock() + self.cfg = dict() + self.cfg['recv-miu'] = options.get('miu', 248) + self.cfg['send-lto'] = options.get('lto', 500) + self.cfg['send-lsc'] = options.get('lsc', 3) + self.cfg['send-agf'] = options.get('agf', True) + self.cfg['llcp-sec'] = options.get('sec', True) + if not sec.OpenSSL: + self.cfg['llcp-sec'] = False + log.debug("llc cfg {0}".format(self.cfg)) + self.sec = None + self.snl = dict({b"urn:nfc:sn:sdp": 1}) + self.sap = 64 * [None] + self.sap[0] = ServiceAccessPoint(0, self) + self.sap[1] = ServiceDiscovery(self) + + def __str__(self): + local = "Local(MIU={miu}, LTO={lto}ms)".format( + miu=self.cfg.get('recv-miu'), lto=self.cfg.get('send-lto')) + remote = "Remote(MIU={miu}, LTO={lto}ms)".format( + miu=self.cfg.get('send-miu'), lto=self.cfg.get('recv-lto')) + return "LLC: {local} {remote}".format(local=local, remote=remote) + + @property + def secure_data_transfer(self): + return self.cfg.get('llcp-dpc', 0) == 1 + + def activate(self, mac, **options): + assert isinstance(mac, (nfc.dep.Initiator, nfc.dep.Target)) + self.mac = None + + wks = 1 + sum([1 << sap for sap in self.snl.values() if sap < 15]) + + send_pax = pdu.ParameterExchange() + send_pax.version = (1, 3) + send_pax.wks = wks + if self.cfg['recv-miu'] != 128: + send_pax.miu = self.cfg['recv-miu'] + if self.cfg['send-lto'] != 100: + send_pax.lto = self.cfg['send-lto'] + if self.cfg['send-lsc'] != 0: + send_pax.lsc = self.cfg['send-lsc'] + if self.cfg['llcp-sec']: + send_pax.dpc = 1 + + gb = b'Ffm' + pdu.encode(send_pax)[2:] + if isinstance(mac, nfc.dep.Initiator): + self.link.CONNECT = True + gb = mac.activate(gbi=gb, **options) + self.run = self.run_as_initiator + else: + self.link.LISTEN = True + gb = mac.activate(gbt=gb, **options) + self.run = self.run_as_target + + if gb and gb.startswith(b'Ffm') and len(gb) >= 6: + if ((isinstance(mac, nfc.dep.Target) + and mac.rwt >= send_pax.lto * 1E-3)): + msg = "local NFC-DEP RWT {0:.3f} contradicts LTO {1:.3f} sec" + log.warning(msg.format(mac.rwt, send_pax.lto*1E3)) + + rcvd_pax = pdu.decode(b"\x00\x40" + bytes(gb[3:])) + + log.debug("SENT {0}".format(send_pax)) + log.debug("RCVD {0}".format(rcvd_pax)) + + self.cfg['rcvd-ver'] = rcvd_pax.version + self.cfg['send-miu'] = rcvd_pax.miu + self.cfg['recv-lto'] = rcvd_pax.lto + self.cfg['send-wks'] = rcvd_pax.wks + self.cfg['send-lsc'] = rcvd_pax.lsc + self.cfg['llcp-dpc'] = rcvd_pax.dpc if self.cfg['llcp-sec'] else 0 + log.debug("llc cfg {0}".format(self.cfg)) + + info = '\n'.join([ + "LLCP Link established as NFC-DEP {role}", + "Local LLCP Settings", + " LLCP Version: {send_pax.version_text}", + " Link Timeout: {send_pax.lto} ms", + " Max Inf Unit: {send_pax.miu} octet", + " Link Service: {send_pax.lsc_text}", + " Data Protect: {send_pax.dpc_text}", + " Service List: {send_pax.wks:016b} ({send_pax.wks_text})", + "Remote LLCP Settings", + " LLCP Version: {rcvd_pax.version[0]}.{rcvd_pax.version[1]}", + " Link Timeout: {rcvd_pax.lto} ms", + " Max Inf Unit: {rcvd_pax.miu} octet", + " Link Service: {rcvd_pax.lsc_text}", + " Data Protect: {rcvd_pax.dpc_text}", + " Service List: {rcvd_pax.wks:016b} ({rcvd_pax.wks_text})" + ]).format(role=mac.role, send_pax=send_pax, rcvd_pax=rcvd_pax) + log.info(info) + + if isinstance(mac, nfc.dep.Initiator) and mac.rwt is not None: + max_rwt = 4096/13.56E6 * 2**10 + if mac.rwt > max_rwt: + msg = "remote NFC-DEP RWT {0:.3f} exceeds max {1:.3f} sec" + log.warning(msg.format(mac.rwt, max_rwt)) + + self.mac = mac + self.link.CONNECTED = True + + return bool(self.mac) + + def terminate(self, reason): + log.debug("llcp link termination caused by {0}".format(reason)) + if type(self.mac) == nfc.dep.Initiator: + if self.link.DISCONNECT is True: + self.exchange(pdu.Disconnect(0, 0), timeout=0.5) + self.mac.deactivate(release=False) # use DESELECT + if type(self.mac) == nfc.dep.Target: + self.mac.deactivate(data=bytearray(b"\x01\x40")) + # shutdown local services + for i in range(63, -1, -1): + if not self.sap[i] is None: + log.debug("closing service access point %d" % i) + self.sap[i].shutdown() + self.sap[i] = None + self.link.SHUTDOWN = True + + def exchange(self, send_pdu, timeout): + # Send and receive one protocol data unit. The send_pdu is + # None for the first call when running as target (because the + # target first receives a pdu). All PDUs except SYMM are + # logged with debug level, SYMM is logged with DEBUG-1 so that + # it must be explicitely enabled. The return value is either a + # PDU instance or None. + try: + if send_pdu: + loglevel = logging.DEBUG - bool(send_pdu.name == "SYMM") + log.log(loglevel, "SEND %s", send_pdu) + send_data = pdu.encode(send_pdu) + self.pcnt.sent[send_pdu.name] += 1 + rcvd_data = self.mac.exchange(send_data, timeout) + else: + rcvd_data = self.mac.exchange(None, timeout) + if rcvd_data is not None: + rcvd_pdu = pdu.decode(rcvd_data) + self.pcnt.rcvd[rcvd_pdu.name] += 1 + loglevel = logging.DEBUG - bool(rcvd_pdu.name == "SYMM") + log.log(loglevel, "RECV %s", rcvd_pdu) + return rcvd_pdu + except (nfc.clf.CommunicationError, pdu.Error) as error: + log.warning("{0!r}".format(error)) + + def run_as_initiator(self, terminate=lambda: False): + recv_timeout = 1E-3 * (self.cfg['recv-lto'] + 10) + msg = "starting initiator run loop with a receive timeout of %.3f sec" + log.debug(msg, recv_timeout) + + symm = 0 # counts the number of consecutive SYMM PDUs + try: + if self.cfg['llcp-dpc'] == 1: + cipher = sec.cipher_suite("ECDH_anon_WITH_AEAD_AES_128_CCM_4") + pubkey = cipher.public_key_x + cipher.public_key_y + random = cipher.random_nonce + send_dps = pdu.DataProtectionSetup(0, 0, pubkey, random) + rcvd_dps = self.exchange(send_dps, recv_timeout) + if not isinstance(rcvd_dps, pdu.DataProtectionSetup): + log.error("expected a DPS PDU response") + return self.terminate(reason="key agreement error") + if not (rcvd_dps.ecpk and len(rcvd_dps.ecpk) == 64): + log.error("absent or invalid ECPK parameter in DPS PDU") + return self.terminate(reason="key agreement error") + if not (rcvd_dps.rn and len(rcvd_dps.rn) == 8): + log.error("absent or invalid RN parameter in DPS PDU") + return self.terminate(reason="key agreement error") + cipher.calculate_session_key(rcvd_dps.ecpk, rn_t=rcvd_dps.rn) + self.sec = cipher + + send_pdu = self.collect(delay=0.01) + self.link.ESTABLISHED = True + while not terminate(): + if send_pdu is None: + send_pdu = pdu.Symmetry() + rcvd_pdu = self.exchange(send_pdu, recv_timeout) + if rcvd_pdu is None: + return self.terminate(reason="link disruption") + if rcvd_pdu == pdu.Disconnect(0, 0): + self.link.CLOSED = True + return self.terminate(reason="remote choice") + symm += 1 if rcvd_pdu.name == "SYMM" else 0 + self.dispatch(rcvd_pdu) + send_pdu = self.collect(delay=0.001) + if send_pdu is None and symm >= 10: + send_pdu = self.collect(delay=0.05) + else: + self.link.DISCONNECT = True + self.terminate(reason="local choice") + except KeyboardInterrupt: + print() # move to new line + self.link.DISCONNECT = True + self.terminate(reason="local choice") + raise KeyboardInterrupt + except IOError: + self.terminate(reason="input/output error") + raise SystemExit + except sec.KeyAgreementError: + self.terminate(reason="key agreement error") + raise SystemExit + except sec.DecryptionError: + self.terminate(reason="decryption error") + raise SystemExit + except sec.EncryptionError: + self.terminate(reason="encryption error") + raise SystemExit + finally: + log.debug("llc run loop terminated on initiator") + + def run_as_target(self, terminate=lambda: False): + recv_timeout = 1E-3 * (self.cfg['recv-lto'] + 10) + msg = "starting target run loop with a receive timeout of %.3f sec" + log.debug(msg, recv_timeout) + + symm = 0 # counts the number of consecutive SYMM PDUs + try: + if self.cfg['llcp-dpc'] == 1: + cipher = sec.cipher_suite("ECDH_anon_WITH_AEAD_AES_128_CCM_4") + pubkey = cipher.public_key_x + cipher.public_key_y + random = cipher.random_nonce + send_dps = pdu.DataProtectionSetup(0, 0, pubkey, random) + rcvd_dps = self.exchange(None, recv_timeout) + if not isinstance(rcvd_dps, pdu.DataProtectionSetup): + log.error("expected a DPS PDU request") + return self.terminate(reason="key agreement error") + if not (rcvd_dps.ecpk and len(rcvd_dps.ecpk) == 64): + log.error("absent or invalid ECPK parameter in DPS PDU") + return self.terminate(reason="key agreement error") + if not (rcvd_dps.rn and len(rcvd_dps.rn) == 8): + log.error("absent or invalid RN parameter in DPS PDU") + return self.terminate(reason="key agreement error") + rcvd_pdu = self.exchange(send_dps, recv_timeout) + cipher.calculate_session_key(rcvd_dps.ecpk, rn_i=rcvd_dps.rn) + self.sec = cipher + else: + rcvd_pdu = self.exchange(None, recv_timeout) + + self.link.ESTABLISHED = True + while not terminate(): + if rcvd_pdu is None: + return self.terminate(reason="link disruption") + if rcvd_pdu == pdu.Disconnect(0, 0): + self.link.CLOSED = True + return self.terminate(reason="remote choice") + symm += 1 if isinstance(rcvd_pdu, pdu.Symmetry) else 0 + self.dispatch(rcvd_pdu) + send_pdu = self.collect(delay=0.001) + if send_pdu is None and symm >= 10: + send_pdu = self.collect(delay=0.05) + if send_pdu is None: + send_pdu = pdu.Symmetry() + rcvd_pdu = self.exchange(send_pdu, recv_timeout) + else: + self.link.DISCONNECT = True + self.terminate(reason="local choice") + except KeyboardInterrupt: + print() # move to new line + self.link.DISCONNECT = True + self.terminate(reason="local choice") + raise KeyboardInterrupt + except IOError: + self.terminate(reason="input/output error") + raise SystemExit + except sec.KeyAgreementError: + self.terminate(reason="key agreement error") + raise SystemExit + except sec.DecryptionError: + self.terminate(reason="decryption error") + raise SystemExit + except sec.EncryptionError: + self.terminate(reason="encryption error") + raise SystemExit + finally: + log.debug("llc run loop terminated on target") + + def collect(self, delay=None): + # Collect a single PDU or multiple PDUs if aggregation is enabled. + if delay: + time.sleep(delay) + + def encrypt(send_pdu): + pdu_type = type(send_pdu) + a = send_pdu.encode_header() + c = self.sec.encrypt(a, send_pdu.data) + return pdu_type(*pdu_type.decode_header(a), data=c) + + miu_size = self.cfg["send-miu"] + icv_size = self.sec.icv_size if self.sec else 0 + send_pdu = None + + with self.lock: + # Dequeue from the list of active SAP until a first PDU is + # returned. The list is sorted to first iterate the raw + # SAPs (raw SAPs do not respect the miu_size value and we + # must avoid them to return PDUs in aggregation). The PDU + # is returned straight if it fills or exceeds the Link + # MIU. Otherwise the loop terminates at this point. The + # sap.dequeue method is called with icv_size=0 because for + # encrypted but not aggregated UI and I PDUs the receiver + # must accept them with complete MIU plus ICV size. + for sap in sorted(filter(None, self.sap), reverse=True, + key=lambda sap: sap.mode == RAW_ACCESS_POINT): + send_pdu = sap.dequeue(miu_size, icv_size=0) + if send_pdu: + if self.sec and send_pdu.name in ("UI", "I"): + send_pdu = encrypt(send_pdu) + if len(send_pdu) - send_pdu.header_size >= miu_size: + return send_pdu + break + + # Data Link Connection endpoints do not dequeue RR/RNR PDUs until + # the receive window is exhausted. If there is not yet a PDU to + # send, this loop allows voluntary acknowledgement. + if send_pdu is None: + for sap in filter(None, self.sap): + if sap.mode == DATA_LINK_CONNECTION: + send_pdu = sap.sendack() + if send_pdu: + break + + # Finish if either there is either no PDU to send or if PDU + # aggregation is disabled. + if send_pdu is None or self.cfg['send-agf'] is False: + return send_pdu + + # We have one PDU to send and aggregation is enabled. We'll see if + # there are more outbound PDUs and collect them into an AGF PDU. + agf_pdu = pdu.AggregatedFrame(0, 0, [send_pdu]) + miu_size = self.cfg["send-miu"] - len(agf_pdu) - 3 + while True: + # The first loop will dequeue PDUs until the reamining miu_size + # is exhausted or all active SAP did not return a PDU. + deq_none = True + for sap in filter(None, self.sap): + send_pdu = sap.dequeue(miu_size, icv_size) + if send_pdu: + deq_none = False + if self.sec and send_pdu.name in ("UI", "I"): + send_pdu = encrypt(send_pdu) + agf_pdu.append(send_pdu) + miu_size = self.cfg["send-miu"] - len(agf_pdu) - 3 + if miu_size < 0: + break + if miu_size < 0 or deq_none: + break + # If the miu_size is not yet exhausted we query all data link + # connection endpoints once for voluntary acknowledgements. + if miu_size >= 0: + for sap in filter(None, self.sap): + if sap.mode == DATA_LINK_CONNECTION: + send_pdu = sap.sendack() + if send_pdu: + agf_pdu.append(send_pdu) + miu_size = self.cfg["send-miu"] - len(agf_pdu) - 3 + if miu_size < 0: + break + + return agf_pdu if agf_pdu.count > 1 else agf_pdu.first + + def dispatch(self, rcvd_pdu): + if rcvd_pdu is None or rcvd_pdu.name == "SYMM": + return + + if rcvd_pdu.name == "AGF": + if rcvd_pdu.dsap == 0 and rcvd_pdu.ssap == 0: + for p in rcvd_pdu: + log.debug(" " + str(p)) + for p in rcvd_pdu: + self.dispatch(p) + return + + if rcvd_pdu.name == "CONNECT" and rcvd_pdu.dsap == 1: + # connect-by-name + addr = self.snl.get(rcvd_pdu.sn) + if not addr or self.sap[addr] is None: + dm_reason = 0x10 if rcvd_pdu.sn is None else 0x02 + dm_pdu = pdu.DisconnectedMode(rcvd_pdu.ssap, 1, dm_reason) + self.sap[1].dmpdu.append(dm_pdu) + log.debug("could not find service %r", rcvd_pdu.sn) + return + # service found, rewrite CONNECT PDU to its DSAP + rcvd_pdu = pdu.Connect(dsap=addr, ssap=rcvd_pdu.ssap, + rw=rcvd_pdu.rw, miu=rcvd_pdu.miu) + + if self.sec and rcvd_pdu.name in ("UI", "I"): + pdu_type = type(rcvd_pdu) + a = rcvd_pdu.encode_header() + p = self.sec.decrypt(a, rcvd_pdu.data) + rcvd_pdu = pdu_type(*pdu_type.decode_header(a), data=p) + + with self.lock: + sap = self.sap[rcvd_pdu.dsap] + if sap: + sap.enqueue(rcvd_pdu) + else: + log.debug("can't dispatch PDU %s", rcvd_pdu) + + def resolve(self, name): + if isinstance(name, (bytes, bytearray)): + return self.sap[1].resolve(bytes(name)) + return self.sap[1].resolve(name.encode('latin')) + + def socket(self, socket_type): + if socket_type == RAW_ACCESS_POINT: + return tco.RawAccessPoint(recv_miu=self.cfg["recv-miu"]) + if socket_type == LOGICAL_DATA_LINK: + return tco.LogicalDataLink(recv_miu=self.cfg["recv-miu"]) + if socket_type == DATA_LINK_CONNECTION: + return tco.DataLinkConnection(recv_miu=128, recv_win=1) + + def setsockopt(self, socket, option, value): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if option == nfc.llcp.SO_RCVMIU: + value = min(value, self.cfg['recv-miu']) + socket.setsockopt(option, value) + return socket.getsockopt(option) + + def getsockopt(self, socket, option): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if isinstance(socket, tco.LogicalDataLink): + # FIXME: set socket send miu when activated + socket.send_miu = self.cfg['send-miu'] + if isinstance(socket, tco.RawAccessPoint): + # FIXME: set socket send miu when activated + socket.send_miu = self.cfg['send-miu'] + return socket.getsockopt(option) + + def bind(self, socket, addr_or_name=None): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if socket.addr is not None: + raise err.Error(errno.EINVAL) + if addr_or_name is None: + self._bind_by_none(socket) + elif isinstance(addr_or_name, int): + self._bind_by_addr(socket, addr_or_name) + elif isinstance(addr_or_name, (bytes, bytearray)): + self._bind_by_name(socket, bytes(addr_or_name)) + elif isinstance(addr_or_name, str): + self._bind_by_name(socket, addr_or_name.encode('latin')) + else: + raise err.Error(errno.EFAULT) + + def _bind_by_none(self, socket): + with self.lock: + try: + addr = 32 + self.sap[32:64].index(None) + except ValueError: + raise err.Error(errno.EAGAIN) + else: + socket.bind(addr) + self.sap[addr] = ServiceAccessPoint(addr, self) + self.sap[addr].insert_socket(socket) + + def _bind_by_addr(self, socket, addr): + if addr < 0 or addr > 63: + raise err.Error(errno.EFAULT) + with self.lock: + if addr in range(32, 64) or isinstance(socket, tco.RawAccessPoint): + if self.sap[addr] is None: + socket.bind(addr) + self.sap[addr] = ServiceAccessPoint(addr, self) + self.sap[addr].insert_socket(socket) + else: + raise err.Error(errno.EADDRINUSE) + else: + raise err.Error(errno.EACCES) + + def _bind_by_name(self, socket, name): + if not service_name_format.match(name): + raise err.Error(errno.EFAULT) + + with self.lock: + if self.snl.get(name) is not None: + raise err.Error(errno.EADDRINUSE) + addr = wks_map.get(name) + if addr is None: + try: + addr = 16 + self.sap[16:32].index(None) + except ValueError: + raise err.Error(errno.EADDRNOTAVAIL) + socket.bind(addr) + self.sap[addr] = ServiceAccessPoint(addr, self) + self.sap[addr].insert_socket(socket) + self.snl[name] = addr + + def connect(self, socket, dest): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if not socket.is_bound: + self.bind(socket) + socket.connect(dest) + log.debug("connected ({0} ===> {1})".format(socket.addr, socket.peer)) + if socket.send_miu > self.cfg['send-miu']: + log.warning("reducing outbound miu to not exceed the link miu") + socket.send_miu = self.cfg['send-miu'] + + def listen(self, socket, backlog): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if not isinstance(socket, tco.DataLinkConnection): + raise err.Error(errno.EOPNOTSUPP) + if not isinstance(backlog, int): + raise TypeError("backlog must be int type") + if backlog < 0: + raise ValueError("backlog can not be negative") + backlog = min(backlog, 16) + if not socket.is_bound: + self.bind(socket) + socket.listen(backlog) + + def accept(self, socket): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if not isinstance(socket, tco.DataLinkConnection): + raise err.Error(errno.EOPNOTSUPP) + while True: + client = socket.accept() + self.sap[client.addr].insert_socket(client) + log.debug("new data link connection ({0} <=== {1})" + .format(client.addr, client.peer)) + if client.send_miu > self.cfg['send-miu']: + log.warning("reducing outbound miu to comply with link miu") + client.send_miu = self.cfg['send-miu'] + return client + + def send(self, socket, message, flags): + return self.sendto(socket, message, socket.peer, flags) + + def sendto(self, socket, message, dest, flags): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if isinstance(socket, tco.RawAccessPoint): + if not isinstance(message, pdu.ProtocolDataUnit): + raise TypeError("on a raw access point message must be a pdu") + if not socket.is_bound: + self.bind(socket) + # FIXME: set socket send miu when activated + socket.send_miu = self.cfg['send-miu'] + return socket.send(message, flags) + if not isinstance(message, (bytes, bytearray)): + raise TypeError("message data must be a bytes-like object") + if isinstance(socket, tco.LogicalDataLink): + if dest is None: + raise err.Error(errno.EDESTADDRREQ) + if not socket.is_bound: + self.bind(socket) + # FIXME: set socket send miu when activated + socket.send_miu = self.cfg['send-miu'] + return socket.sendto(message, dest, flags) + if isinstance(socket, tco.DataLinkConnection): + return socket.send(message, flags) + + def recv(self, socket): + message, sender = self.recvfrom(socket) + return message + + def recvfrom(self, socket): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if not (socket.addr and self.sap[socket.addr]): + raise err.Error(errno.EBADF) + if isinstance(socket, tco.RawAccessPoint): + return (socket.recv(), None) + if isinstance(socket, tco.LogicalDataLink): + return socket.recvfrom() + if isinstance(socket, tco.DataLinkConnection): + return (socket.recv(), socket.peer) + + def poll(self, socket, event, timeout=None): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if not (socket.addr and self.sap[socket.addr]): + raise err.Error(errno.EBADF) + return socket.poll(event, timeout) + + def close(self, socket): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if socket.is_bound: + self.sap[socket.addr].remove_socket(socket) + else: + socket.close() + + def getsockname(self, socket): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + return socket.addr + + def getpeername(self, socket): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + return socket.peer diff --git a/src/lib/nfc/llcp/pdu.py b/src/lib/nfc/llcp/pdu.py new file mode 100644 index 0000000..1ca160e --- /dev/null +++ b/src/lib/nfc/llcp/pdu.py @@ -0,0 +1,945 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import struct +from binascii import hexlify + +import logging +log = logging.getLogger(__name__) + + +class Error(Exception): + pass + + +class DecodeError(Error): + pass + + +class EncodeError(Error): + pass + + +class Parameter: + VERSION, MIUX, WKS, LTO, RW, SN, OPT, SDREQ, SDRES, ECPK, RN = range(1, 12) + + @staticmethod + def decode(data, offset): + try: + T, L = struct.unpack_from('BB', data, offset) + V = struct.unpack_from('%ds' % L, data, offset+2)[0] + except struct.error as error: + msg = " while decoding TLV %r" % hexlify(data[offset:]) + raise DecodeError(str(error) + msg) + + if T == Parameter.VERSION: + if L != 1: + raise DecodeError("VERSION TLV length error") + V = struct.unpack('B', V)[0] + elif T == Parameter.MIUX: + if L != 2: + raise DecodeError("MIUX TLV length error") + V = struct.unpack('>H', V)[0] + if V & 0xF800: + log.warning("MIUX TLV reserved bits set") + V = V & 0x07FF + elif T == Parameter.WKS: + if L != 2: + raise DecodeError("WKS TLV length error") + V = struct.unpack('>H', V)[0] + elif T == Parameter.LTO: + if L != 1: + raise DecodeError("LTO TLV length error") + V = struct.unpack('B', V)[0] + elif T == Parameter.RW: + if L != 1: + raise DecodeError("RW TLV length error") + V = struct.unpack('B', V)[0] + if V & 0xF0: + log.warning("RW TLV reserved bits set") + V = V & 0x0F + elif T == Parameter.SN and L == 0: + log.warning("SN TLV with zero-length service name") + elif T == Parameter.OPT: + if L != 1: + raise DecodeError("OPT TLV length error") + V = struct.unpack_from('B', V)[0] + if V & 0xF8: + log.warning("OPT TLV reserved bits set") + V = V & 0x07 + elif T == Parameter.SDREQ: + if L == 0: + raise DecodeError("SDREQ TLV length error") + if L == 1: + log.warning("SDREQ TLV with zero-length service name") + V = struct.unpack('B%ds' % (L-1), V) + elif T == Parameter.SDRES: + if L != 2: + raise DecodeError("SDRES TLV length error") + V = struct.unpack('BB', V) + elif T == Parameter.ECPK: + if L == 0: + log.warning("ECPK TLV with zero-length value") + if L & 1: + log.warning("ECPK TLV with odd length value") + elif T == Parameter.RN: + if L == 0: + log.warning("RN TLV with zero-length value") + + return (T, L, V) + + @staticmethod + def encode(T, V): + try: + if T in (Parameter.VERSION, Parameter.LTO, + Parameter.RW, Parameter.OPT): + return struct.pack('BBB', T, 1, V) + if T in (Parameter.MIUX, Parameter.WKS): + return struct.pack('>BBH', T, 2, V) + if T in (Parameter.SN, Parameter.ECPK, Parameter.RN): + if len(V) > 255: + raise EncodeError("can't encode TLV T=%d, V=%r" % (T, V)) + return struct.pack('BB', T, len(V)) + bytes(V) + if T == Parameter.SDREQ: + tid, sn = V[0], V[1] + if len(sn) > 254: + raise EncodeError("can't encode TLV T=%d, V=%r" % (T, V)) + return struct.pack('>BBB', T, 1+len(sn), tid) + bytes(sn) + if T == Parameter.SDRES: + tid, sap = V[0], V[1] + return struct.pack('>BBBB', T, 2, tid, sap) + raise EncodeError("unknown TLV T=%d, V=%r" % (T, V)) + except struct.error as error: + msg = " for TLV T=%d, V=%r" % (T, V) + raise EncodeError(str(error) + msg) + + +# ----------------------------------------------------------------------------- +# ProtocolDataUnit Base Class +# ----------------------------------------------------------------------------- +class ProtocolDataUnit(object): + header_size = 2 + + def __init__(self, ptype, dsap, ssap): + self.ptype = ptype + self.dsap = dsap + self.ssap = ssap + + @classmethod + def decode_header(cls, data, offset=0, size=None): + if size is None: + size = len(data) - offset + if size < cls.header_size: + raise DecodeError("insufficient pdu header bytes") + (dsap, ssap) = struct.unpack_from('!BB', data, offset) + return (dsap >> 2, ssap & 63) + + def encode_header(self): + if self.dsap is None or self.ssap is None: + raise EncodeError("pdu dsap and ssap field can not be None") + if self.dsap < 0 or self.ssap < 0: + raise EncodeError("pdu dsap and ssap field can not be < 0") + if self.dsap > 63 or self.ssap > 63: + raise EncodeError("pdu dsap and ssap field can not be > 63") + return struct.pack('!H', self.dsap << 10 | self.ptype << 6 | self.ssap) + + def __eq__(self, other): + return self.encode() == other.encode() + + def __str__(self): + string = "{pdu.ssap:2} -> {pdu.dsap:2} {pdu.name:4.4s}" + return string.format(pdu=self) + + +# ----------------------------------------------------------------------------- +# NumberedProtocolDataUnit Base Class +# ----------------------------------------------------------------------------- +class NumberedProtocolDataUnit(ProtocolDataUnit): + header_size = 3 + + def __init__(self, ptype, dsap, ssap, ns, nr): + super(NumberedProtocolDataUnit, self).__init__(ptype, dsap, ssap) + self.ns, self.nr = ns, nr + + @classmethod + def decode_header(cls, data, offset=0, size=None): + if size is None: + size = len(data) - offset + if size < cls.header_size: + raise DecodeError("numbered pdu header length error") + (dsap, ssap, sequence) = struct.unpack_from('!BBB', data, offset) + return (dsap >> 2, ssap & 63, sequence >> 4, sequence & 15) + + def encode_header(self): + data = super(NumberedProtocolDataUnit, self).encode_header() + if self.ns is None or self.nr is None: + raise EncodeError("pdu ns and nr field can not be None") + if self.ns < 0 or self.nr < 0: + raise EncodeError("pdu ns and nr field can not be < 0") + if self.ns > 15 or self.nr > 15: + raise EncodeError("pdu ns and nr field can not be > 15") + return data + struct.pack('!B', self.ns << 4 | self.nr) + + def __len__(self): + return 3 + + def __str__(self): + f = " N(R)={p.nr}" if self.ns is None else " N(S)={p.ns} N(R)={p.nr}" + return super(NumberedProtocolDataUnit, self).__str__()+f.format(p=self) + + +# ----------------------------------------------------------------------------- +# Symmetry PDU +# ----------------------------------------------------------------------------- +class Symmetry(ProtocolDataUnit): + name = "SYMM" + + def __init__(self, dsap=0, ssap=0): + super(Symmetry, self).__init__(0b0000, dsap, ssap) + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + if dsap != 0 or ssap != 0: + raise DecodeError("SSAP and DSAP must be 0 in SYMM PDU") + if size >= 3: + raise DecodeError("SYMM PDU PAYLOAD must be empty") + return Symmetry(dsap, ssap) + + def encode(self): + if self.dsap != 0 or self.ssap != 0: + raise EncodeError("SSAP and DSAP must be 0 in SYMM PDU") + return self.encode_header() + + def __len__(self): + return 2 + + def __str__(self): + return super(Symmetry, self).__str__() + + +# ----------------------------------------------------------------------------- +# Parameter Exchange PDU +# ----------------------------------------------------------------------------- +class ParameterExchange(ProtocolDataUnit): + name = "PAX" + + def __init__(self, dsap=0, ssap=0, version=None, miux=None, + wks=None, lto=None, opt=None): + super(ParameterExchange, self).__init__(0b0001, dsap, ssap) + self._version = version + self._miux = miux + self._wks = wks + self._lto = lto + self._opt = opt + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + if dsap != 0 or ssap != 0: + raise DecodeError("SSAP and DSAP must be 0 in PAX PDU") + pax_pdu = ParameterExchange(dsap, ssap) + offset, size = offset + 2, size - 2 + while size >= 2: + T, L, V = Parameter.decode(data, offset) + if T == Parameter.VERSION: + pax_pdu._version = V + elif T == Parameter.MIUX: + pax_pdu._miux = V + elif T == Parameter.WKS: + pax_pdu._wks = V + elif T == Parameter.LTO: + pax_pdu._lto = V + elif T == Parameter.OPT: + pax_pdu._opt = V + else: + log.warning("invalid TLV %r in PAX PDU", (T, L, V)) + offset, size = offset + 2 + L, size - 2 - L + return pax_pdu + + def encode(self): + if self.dsap != 0 or self.ssap != 0: + raise EncodeError("SSAP and DSAP must be 0 in PAX PDU") + data = self.encode_header() + if self._version is not None: + data += Parameter.encode(Parameter.VERSION, self._version) + if self._miux is not None: + data += Parameter.encode(Parameter.MIUX, self._miux) + if self._wks is not None: + data += Parameter.encode(Parameter.WKS, self._wks) + if self._lto is not None: + data += Parameter.encode(Parameter.LTO, self._lto) + if self._opt is not None: + data += Parameter.encode(Parameter.OPT, self._opt) + return data + + def __len__(self): + return (2 + + (3 if self._version is not None else 0) + + (4 if self._miux is not None else 0) + + (4 if self._wks is not None else 0) + + (3 if self._lto is not None else 0) + + (3 if self._opt is not None else 0)) + + @property + def version(self): + version = self._version + return (version >> 4, version & 15) if version else (0, 0) + + @version.setter + def version(self, value): + self._version = (value[0] << 4 & 0xF0) | (value[1] & 0x0F) + + @property + def version_text(self): + return "{0}.{1}".format(*self.version) + + @property + def miu(self): + return self._miux + 128 if self._miux is not None else 128 + + @miu.setter + def miu(self, value): + self._miux = max(value - 128, 0) + + @property + def wks(self): + return self._wks if self._wks is not None else 0 + + @wks.setter + def wks(self, value): + self._wks = value & 0xFFFF + + @property + def wks_text(self): + t = {0: "LLC", 1: "SDP", 4: "SNEP"} + return ', '.join([ + t.get(i, str(i)) for i in range(15, -1, -1) if self.wks >> i & 1]) + + @property + def lto(self): + return (self._lto if self._lto is not None else 10) * 10 + + @lto.setter + def lto(self, value): + self._lto = (value // 10) & 0xFF + + @property + def lsc(self): + return self._opt & 3 if self._opt is not None else 0 + + @lsc.setter + def lsc(self, value): + self._opt = ((self._opt or 0) & 0b11111100) | (value & 0b00000011) + + @property + def lsc_text(self): + return ("link service class unknown at activation", + "connection-less link service only", + "connection-oriented link service only", + "connection-less and connection-oriented")[self.lsc] + + @property + def dpc(self): + return self._opt >> 2 & 1 if self._opt is not None else 0 + + @dpc.setter + def dpc(self, value): + self._opt = ((self._opt or 0) & 0b11111011) | (bool(value) << 2) + + @property + def dpc_text(self): + return ("secure data transfer mode not supported", + "secure data transfer mode is supported")[self.dpc] + + def __str__(self): + s = super(ParameterExchange, self).__str__() + if self._version is not None: + s += " VER={0}.{1}".format(*self.version) + if self._wks is not None: + s += " WKS={0:016b}".format(self._wks) + if self._miux is not None: + s += " MIUX={0}".format(self._miux) + if self._lto is not None: + s += " LTO={0}".format(self._lto) + if self._opt is not None: + s += " OPT={0:08b}".format(self._opt) + return s + + +# ----------------------------------------------------------------------------- +# Aggregated Frame PDU +# ----------------------------------------------------------------------------- +class AggregatedFrame(ProtocolDataUnit): + name = "AGF" + + def __init__(self, dsap=0, ssap=0, aggregate=[]): + super(AggregatedFrame, self).__init__(0b0010, dsap, ssap) + self._aggregate = aggregate[:] + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + if dsap != 0 or ssap != 0: + raise DecodeError("SSAP and DSAP must be 0 in AGF PDU") + agf_pdu = AggregatedFrame(dsap, ssap) + offset, size = offset + 2, size - 2 + while size > 0: + try: + (pdu_size,) = struct.unpack_from('!H', data, offset) + except struct.error: + raise DecodeError("aggregated PDU length field error in AGF") + agf_pdu.append(decode(data, offset+2, pdu_size)) + offset, size = offset + 2 + pdu_size, size - 2 - pdu_size + return agf_pdu + + def encode(self): + if self.dsap != 0 or self.ssap != 0: + raise EncodeError("SSAP and DSAP must be 0 in AGF PDU") + data = self.encode_header() + for encoded_pdu in [pdu.encode() for pdu in self._aggregate]: + data += struct.pack('!H', len(encoded_pdu)) + encoded_pdu + return data + + def append(self, pdu): + self._aggregate.append(pdu) + + @property + def count(self): + return len(self._aggregate) + + @property + def first(self): + return self._aggregate[0] + + def __len__(self): + return 2 + sum([2+len(pdu) for pdu in self._aggregate]) + + def __str__(self): + def s(p): + return "LEN={0} '".format(len(p)) + \ + ProtocolDataUnit.__str__(p).rstrip() + "'" + return super(AggregatedFrame, self).__str__() + \ + " LEN={0} [".format(len(self)-2) + \ + " ".join([s(p) for p in self._aggregate]) + "]" + + def __iter__(self): + return AggregatedFrameIterator(self._aggregate) + + +class AggregatedFrameIterator(object): + def __init__(self, aggregate): + self._aggregate = aggregate + self._current = 0 + + def __iter__(self): + return self + + def __next__(self): + if self._current == len(self._aggregate): + raise StopIteration + self._current += 1 + return self._aggregate[self._current-1] + + def next(self): + return self.__next__() + + +# ----------------------------------------------------------------------------- +# Unnumbered Information PDU +# ----------------------------------------------------------------------------- +class UnnumberedInformation(ProtocolDataUnit): + name = "UI" + + def __init__(self, dsap, ssap, data=None): + super(UnnumberedInformation, self).__init__(0b0011, dsap, ssap) + self.data = data if data else b'' + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + payload = bytes(data[offset+2:offset+size]) + return UnnumberedInformation(dsap, ssap, payload) + + def encode(self): + return self.encode_header() + bytes(self.data) + + def __len__(self): + return 2 + len(self.data) + + def __str__(self): + return super(UnnumberedInformation, self).__str__() + \ + " LEN={0} DATA={1}".format(len(self.data), hexlify(self.data)) + + +# ----------------------------------------------------------------------------- +# Connect PDU +# ----------------------------------------------------------------------------- +class Connect(ProtocolDataUnit): + name = "CONNECT" + + def __init__(self, dsap, ssap, miu=128, rw=1, sn=None): + super(Connect, self).__init__(0b0100, dsap, ssap) + self.miu = miu + self.rw = rw + self.sn = sn + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + connect_pdu = Connect(dsap, ssap) + offset, size = offset + 2, size - 2 + while size >= 2: + T, L, V = Parameter.decode(data, offset) + if T == Parameter.MIUX: + connect_pdu.miu = 128 + V + elif T == Parameter.RW: + connect_pdu.rw = V + elif T == Parameter.SN: + connect_pdu.sn = V + else: + log.warning("invalid TLV %r in CONNECT PDU", (T, L, V)) + offset, size = offset + 2 + L, size - 2 - L + return connect_pdu + + def encode(self): + data = self.encode_header() + if self.miu and self.miu > 128: + data += Parameter.encode(Parameter.MIUX, self.miu - 128) + if self.rw and self.rw != 1: + data += Parameter.encode(Parameter.RW, self.rw) + if self.sn: + data += Parameter.encode(Parameter.SN, self.sn) + return data + + def __len__(self): + return (2 + + (4 if self.miu and self.miu > 128 else 0) + + (3 if self.rw and self.rw != 1 else 0) + + (2 + len(self.sn) if self.sn else 0)) + + def __str__(self): + s = " MIU={conn.miu} RW={conn.rw} SN={conn.sn}" + return super(Connect, self).__str__() + s.format(conn=self) + + +# ----------------------------------------------------------------------------- +# Disconnect PDU +# ----------------------------------------------------------------------------- +class Disconnect(ProtocolDataUnit): + name = "DISC" + + def __init__(self, dsap, ssap): + super(Disconnect, self).__init__(0b0101, dsap, ssap) + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + return Disconnect(dsap, ssap) + + def encode(self): + return self.encode_header() + + def __len__(self): + return 2 + + def __str__(self): + return super(Disconnect, self).__str__() + + +# ----------------------------------------------------------------------------- +# Connection Complete PDU +# ----------------------------------------------------------------------------- +class ConnectionComplete(ProtocolDataUnit): + name = "CC" + + def __init__(self, dsap, ssap, miu=128, rw=1): + super(ConnectionComplete, self).__init__(0b0110, dsap, ssap) + self.miu = miu + self.rw = rw + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + cc_pdu = ConnectionComplete(dsap, ssap) + offset, size = offset + 2, size - 2 + while size >= 2: + T, L, V = Parameter.decode(data, offset) + if T == Parameter.MIUX: + cc_pdu.miu = 128 + V + elif T == Parameter.RW: + cc_pdu.rw = V + else: + log.warning("invalid TLV %r in CC PDU", (T, L, V)) + offset, size = offset + 2 + L, size - 2 - L + return cc_pdu + + def encode(self): + data = self.encode_header() + if self.miu and self.miu > 128: + data += Parameter.encode(Parameter.MIUX, self.miu - 128) + if self.rw and self.rw != 1: + data += Parameter.encode(Parameter.RW, self.rw) + return data + + def __len__(self): + return (2 + + (4 if self.miu and self.miu > 128 else 0) + + (3 if self.rw and self.rw != 1 else 0)) + + def __str__(self): + return super(ConnectionComplete, self).__str__() + \ + " MIU={cc.miu} RW={cc.rw}".format(cc=self) + + +# ----------------------------------------------------------------------------- +# Disconnected Mode PDU +# ----------------------------------------------------------------------------- +class DisconnectedMode(ProtocolDataUnit): + name = "DM" + + def __init__(self, dsap, ssap, reason=0): + super(DisconnectedMode, self).__init__(0b0111, dsap, ssap) + self.reason = reason + + @classmethod + def decode(cls, data, offset, size): + if size != 3: + raise DecodeError("DM PDU length error") + dsap, ssap = cls.decode_header(data, offset, size) + (reason,) = struct.unpack_from('!B', data, offset+2) + return DisconnectedMode(dsap, ssap, reason) + + def encode(self): + return self.encode_header() + struct.pack('!B', self.reason) + + def __len__(self): + return 3 + + def __str__(self): + return super(DisconnectedMode, self).__str__() + \ + " REASON={dm.reason:02x}h".format(dm=self) + + @property + def reason_text(self): + return { + 0x00: "disconnected", + 0x01: "inactive", + 0x02: "unbound", + 0x03: "rejected", + 0x10: "permanent reject for sap", + 0x11: "permanent reject for any", + 0x20: "temporary reject for sap", + 0x21: "temporary reject for any", + }.get(self.reason, "{0:02x}h".format(self.reason)) + + +# ----------------------------------------------------------------------------- +# Frame Reject PDU +# ----------------------------------------------------------------------------- +class FrameReject(ProtocolDataUnit): + name = "FRMR" + + def __init__(self, dsap, ssap, flags=0, ptype=0, + ns=0, nr=0, vs=0, vr=0, vsa=0, vra=0): + super(FrameReject, self).__init__(0b1000, dsap, ssap) + self.rej_flags = flags + self.rej_ptype = ptype + self.ns = ns + self.nr = nr + self.vs = vs + self.vr = vr + self.vsa = vsa + self.vra = vra + + @classmethod + def decode(cls, data, offset, size): + if size != 6: + raise DecodeError("FRMR PDU length error") + dsap, ssap = cls.decode_header(data, offset, size) + (b0, b1, b2, b3) = struct.unpack_from('!BBBB', data, offset+2) + flags, ptype = b0 >> 4, b0 & 15 + ns, nr = b1 >> 4, b1 & 15 + vs, vr = b2 >> 4, b2 & 15 + vsa, vra = b3 >> 4, b3 & 15 + return FrameReject(dsap, ssap, flags, ptype, ns, nr, vs, vr, vsa, vra) + + @staticmethod + def from_pdu(pdu, flags, dlc): + rej_ptype = pdu.ptype + rej_flags = sum([1 << "SRIW".index(f) for f in flags]) + frmr = FrameReject(pdu.ssap, pdu.dsap, rej_flags, rej_ptype) + if isinstance(pdu, Information): + frmr.ns, frmr.nr = pdu.ns, pdu.nr + if isinstance(pdu, ReceiveReady) or isinstance(pdu, ReceiveNotReady): + frmr.nr = pdu.nr + frmr.vs, frmr.vsa = dlc.send_cnt, dlc.send_ack + frmr.vr, frmr.vra = dlc.recv_cnt, dlc.recv_ack + return frmr + + def encode(self): + return self.encode_header() + struct.pack( + '!BBBB', self.rej_flags << 4 | self.rej_ptype, + self.ns << 4 | self.nr, self.vs << 4 | self.vr, + self.vsa << 4 | self.vra) + + def __len__(self): + return 6 + + def __str__(self): + return super(FrameReject, self).__str__() +\ + " FLAGS={frmr.rej_flags:04b} PTYPE={frmr.rej_ptype:04b}"\ + " N(S)={frmr.ns} N(R)={frmr.nr}"\ + " V(S)={frmr.vs} V(R)={frmr.vr}"\ + " V(SA)={frmr.vsa} V(RA)={frmr.vra}"\ + .format(frmr=self) + + +# ----------------------------------------------------------------------------- +# Service Name Lookup PDU +# ----------------------------------------------------------------------------- +class ServiceNameLookup(ProtocolDataUnit): + name = "SNL" + + def __init__(self, dsap, ssap, sdreq=None, sdres=None): + super(ServiceNameLookup, self).__init__(0b1001, dsap, ssap) + self.sdreq = sdreq if sdreq else list() + self.sdres = sdres if sdres else list() + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + if dsap != 1 or ssap != 1: + raise DecodeError("SSAP and DSAP must be 1 in SNL PDU") + snl_pdu = ServiceNameLookup(dsap, ssap) + offset, size = offset + 2, size - 2 + while size >= 2: + T, L, V = Parameter.decode(data, offset) + if T == Parameter.SDREQ: + snl_pdu.sdreq.append(V) + elif T == Parameter.SDRES: + snl_pdu.sdres.append(V) + else: + log.warning("invalid TLV %r in SNL PDU", (T, L, V)) + offset, size = offset + 2 + L, size - 2 - L + return snl_pdu + + def encode(self): + data = self.encode_header() + for sdreq in self.sdreq: + data += Parameter.encode(Parameter.SDREQ, sdreq) + for sdres in self.sdres: + data += Parameter.encode(Parameter.SDRES, sdres) + return data + + def __len__(self): + return 2 + (len(self.sdres) * 4) \ + + sum([3+len(sdreq[1]) for sdreq in self.sdreq]) + + def __str__(self): + return super(ServiceNameLookup, self).__str__() + \ + " SDRES={0} SDREQ={1}".format(str(self.sdres), str(self.sdreq)) + + +# ----------------------------------------------------------------------------- +# Data Protection Setup PDU +# ----------------------------------------------------------------------------- +class DataProtectionSetup(ProtocolDataUnit): + name = "DPS" + + def __init__(self, dsap, ssap, ecpk=None, rn=None): + super(DataProtectionSetup, self).__init__(0b1010, dsap, ssap) + self.ecpk = ecpk + self.rn = rn + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + if dsap != 0 or ssap != 0: + raise DecodeError("SSAP and DSAP must be 0 in DPS PDU") + dps_pdu = DataProtectionSetup(dsap, ssap) + offset, size = offset + 2, size - 2 + while size >= 2: + T, L, V = Parameter.decode(data, offset) + if T == Parameter.ECPK: + dps_pdu.ecpk = V + elif T == Parameter.RN: + dps_pdu.rn = V + else: + log.debug("unknown TLV %r in DPS PDU", (T, L, V)) + offset, size = offset + 2 + L, size - 2 - L + return dps_pdu + + def encode(self): + if self.dsap != 0 or self.ssap != 0: + raise EncodeError("SSAP and DSAP must be 0 in DPS PDU") + data = self.encode_header() + if self.ecpk: + data += Parameter.encode(Parameter.ECPK, self.ecpk) + if self.rn: + data += Parameter.encode(Parameter.RN, self.rn) + return data + + def __len__(self): + return (2 + + (2 + len(self.ecpk) if self.ecpk else 0) + + (2 + len(self.rn) if self.rn else 0)) + + def __str__(self): + return super(DataProtectionSetup, self).__str__() + \ + " ECPK={0} RN={1}".format( + 'None' if self.ecpk is None else hexlify(self.ecpk).decode(), + 'None' if self.rn is None else hexlify(self.rn).decode()) + + +# ----------------------------------------------------------------------------- +# Information PDU +# ----------------------------------------------------------------------------- +class Information(NumberedProtocolDataUnit): + name = "I" + + def __init__(self, dsap, ssap, ns=None, nr=None, data=None): + super(Information, self).__init__(0b1100, dsap, ssap, ns, nr) + self.data = data if data else b'' + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap, ns, nr = cls.decode_header(data, offset, size) + payload = bytes(data[offset+3:offset+size]) + return cls(dsap, ssap, ns, nr, payload) + + def encode(self): + return self.encode_header() + bytes(self.data) + + def __len__(self): + return 3 + len(self.data) + + def __str__(self): + return (super(Information, self).__str__() + " LEN={0} DATA={1}" + .format(len(self.data), hexlify(self.data))) + + +# ----------------------------------------------------------------------------- +# Receive Ready PDU +# ----------------------------------------------------------------------------- +class ReceiveReady(NumberedProtocolDataUnit): + name = "RR" + + def __init__(self, dsap, ssap, nr=None): + super(ReceiveReady, self).__init__(0b1101, dsap, ssap, 0, nr) + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap, ns, nr = cls.decode_header(data, offset, size) + if ns != 0: + log.warning("reserved bits set in sequence field") + return cls(dsap, ssap, nr) + + def encode(self): + return self.encode_header() + + +# ----------------------------------------------------------------------------- +# Receive Not Ready PDU +# ----------------------------------------------------------------------------- +class ReceiveNotReady(NumberedProtocolDataUnit): + name = "RNR" + + def __init__(self, dsap, ssap, nr): + super(ReceiveNotReady, self).__init__(0b1110, dsap, ssap, 0, nr) + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap, ns, nr = cls.decode_header(data, offset, size) + if ns != 0: + log.warning("reserved bits set in sequence field") + return cls(dsap, ssap, nr) + + def encode(self): + return self.encode_header() + + +# ----------------------------------------------------------------------------- +# UnknownProtocolDataUnit +# ----------------------------------------------------------------------------- +class UnknownProtocolDataUnit(ProtocolDataUnit): + def __init__(self, ptype, dsap, ssap, payload): + super(UnknownProtocolDataUnit, self).__init__(ptype, dsap, ssap) + self.name = "{0:04b}".format(ptype) + self.payload = payload + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + pdutype = (data[offset] << 2 | data[offset+1] >> 6) & 0x0F + payload = data[offset+2:offset+size] + return cls(pdutype, dsap, ssap, payload) + + def encode(self): + return self.encode_header() + bytes(self.payload) + + def __len__(self): + return 2 + len(self.payload) + + def __str__(self): + return (super(UnknownProtocolDataUnit, self).__str__() + + " PAYLOAD={}".format(hexlify(self.payload).decode())) + + +# ----------------------------------------------------------------------------- +# pdu decode and encode functions +# ----------------------------------------------------------------------------- +pdu_type_map = { + 0b0000: Symmetry, + 0b0001: ParameterExchange, + 0b0010: AggregatedFrame, + 0b0011: UnnumberedInformation, + 0b0100: Connect, + 0b0101: Disconnect, + 0b0110: ConnectionComplete, + 0b0111: DisconnectedMode, + 0b1000: FrameReject, + 0b1001: ServiceNameLookup, + 0b1010: DataProtectionSetup, + 0b1100: Information, + 0b1101: ReceiveReady, + 0b1110: ReceiveNotReady, +} + + +def decode(data, offset=0, size=None): + size = len(data) if size is None else size + + if offset + size > len(data): + raise DecodeError("size bytes from offset exceed the data length") + if size < 2: + raise DecodeError("less than two header bytes can't make a valid pdu") + + ptype = (struct.unpack_from('>H', data, offset)[0] >> 6) & 0b1111 + pdu_type = pdu_type_map.get(ptype, UnknownProtocolDataUnit) + return pdu_type.decode(data, offset, size) + + +def encode(pdu): + if not isinstance(pdu, ProtocolDataUnit): + raise AttributeError("can't encode %s" % type(pdu)) + + return pdu.encode() diff --git a/src/lib/nfc/llcp/sec.py b/src/lib/nfc/llcp/sec.py new file mode 100644 index 0000000..7fb6bc4 --- /dev/null +++ b/src/lib/nfc/llcp/sec.py @@ -0,0 +1,542 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import struct +import ctypes +import ctypes.util +from ctypes import c_void_p, c_int +from binascii import hexlify + +import logging +log = logging.getLogger(__name__) + +OpenSSL = None + + +class Error(Exception): + pass + + +class EncryptionError(Error): + pass + + +class DecryptionError(Error): + pass + + +class KeyAgreementError(Error): + pass + + +def cipher_suite(name): + if name == "ECDH_anon_WITH_AEAD_AES_128_CCM_4": + return CipherSuite1() + + +class CipherSuite1: + _ccm_t = 4 + _ccm_q = 2 + _ccm_n = 13 + + def __init__(self): + self.random_nonce = None + self.public_key_x = None + self.public_key_y = None + ec_key = OpenSSL.EC_KEY.new_by_curve_name(OpenSSL.NID_X9_62_prime256v1) + if ec_key and ec_key.generate_key() and ec_key.check_key(): + pubkey = ec_key.get_public_key() + x, y = pubkey.get_affine_coordinates_GFp(ec_key.get_group()) + self.public_key_x = x + self.public_key_y = y + self.random_nonce = OpenSSL.rand_bytes(8) + self._ec_key = ec_key + + def calculate_session_key(self, ecpk, rn_i=None, rn_t=None): + if ecpk is None: + raise KeyAgreementError("remote public key is required") + if len(ecpk) != 64: + raise KeyAgreementError("remote public key has wrong size") + if rn_i is None and rn_t is None: + raise KeyAgreementError("remote random nonce is required") + if rn_i and len(rn_i) != 8: + raise KeyAgreementError("initiator random nonce has wrong size") + if rn_t and len(rn_t) != 8: + raise KeyAgreementError("target random nonce has wrong size") + + if rn_i is None: + rn_i = self.random_nonce + if rn_t is None: + rn_t = self.random_nonce + + ec_key = OpenSSL.EC_KEY.new_by_curve_name(OpenSSL.NID_X9_62_prime256v1) + try: + ec_key.set_public_key_affine_coordinates(ecpk[:32], ecpk[32:]) + except AssertionError: + raise KeyAgreementError("remote public key is not on curve") + + cipher = OpenSSL.EVP_aes_128_cbc() + secret = OpenSSL.ECDH(self._ec_key) \ + .compute_key(ec_key.get_public_key()) + k_encr = OpenSSL.CMAC(cipher) \ + .init(rn_i+rn_t) \ + .update(secret).final() + + log.debug("remote ecpk-x %r", hexlify(ecpk[:32])) + log.debug("remote ecpk-y %r", hexlify(ecpk[32:])) + log.debug("shared secret %r", hexlify(secret)) + log.debug("shared nonce %r", hexlify(rn_i+rn_t)) + log.debug("session key %r", hexlify(k_encr)) + + self._pcs = self._pcr = 0 + self._k_encr = k_encr + return self._k_encr + + @property + def icv_size(self): + return self._ccm_t + + def encrypt(self, a, p): + # The nonce N is a leftmost 40-bit fixed part all bits zero + # and a rightmost 64-bit counter part taken from PC(S). + nonce = struct.pack('!xxxxxQ', self._pcs) + if self._pcs < 0xFFFFFFFFFFFFFFFF: + self._pcs += 1 + else: + raise EncryptionError("send counter out of range") + + # The encryption key was computed in calculate_session_key() + key = self._k_encr + + # OpenSSLWrapper methods raise AssertionError when any of the + # operations failed. + try: + return self._encrypt(bytes(a), bytes(p), key, nonce, self._ccm_t) + except AssertionError: + error = "encrypt failed for message %d" % self._pcs + log.error(error) + raise EncryptionError(error) + + @staticmethod + def _encrypt(aad, txt, key, nonce, tlen): + # from https://wiki.openssl.org/index.php/ + # EVP_Authenticated_Encryption_and_Decryption# + # Authenticated_Encryption_using_CCM_mode + evp = OpenSSL.EVP() + evp.encrypt_init(OpenSSL.EVP_aes_128_ccm()) + evp.cipher_ctx.ctrl_set(OpenSSL.EVP.CTRL_CCM_SET_IVLEN, len(nonce)) + evp.cipher_ctx.ctrl_set(OpenSSL.EVP.CTRL_CCM_SET_TAG, tlen) + evp.encrypt_init(key=key, iv=nonce) + evp.encrypt_update(None, None, len(txt)) + evp.encrypt_update(None, aad, len(aad)) + return evp.encrypt_update(len(txt), txt, len(txt)) + \ + evp.cipher_ctx.ctrl_get(OpenSSL.EVP.CTRL_CCM_GET_TAG, tlen) + + def decrypt(self, a, c): + # The nonce N is a leftmost 40-bit fixed part all bits zero + # and a rightmost 64-bit counter part taken from PC(R). + nonce = struct.pack('!xxxxxQ', self._pcr) + if self._pcr < 0xFFFFFFFFFFFFFFFF: + self._pcr += 1 + else: + raise DecryptionError("recv counter out of range") + + # The decryption key was computed in calculate_session_key() + key = self._k_encr + + # OpenSSLWrapper methods raise AssertionError when any of the + # operations failed. + try: + return self._decrypt(bytes(a), bytes(c), key, nonce, self._ccm_t) + except AssertionError: + error = "decrypt failed for message %d" % self._pcr + log.error(error) + raise DecryptionError(error) + + @staticmethod + def _decrypt(aad, txt, key, nonce, tlen): + # from https://wiki.openssl.org/index.php/ + # EVP_Authenticated_Encryption_and_Decryption# + # Authenticated_Decryption_using_CCM_mode + tag = txt[-tlen:] + txt = txt[:-tlen] + evp = OpenSSL.EVP() + evp.decrypt_init(OpenSSL.EVP_aes_128_ccm()) + evp.cipher_ctx.ctrl_set(OpenSSL.EVP.CTRL_CCM_SET_IVLEN, len(nonce)) + evp.cipher_ctx.ctrl_set(OpenSSL.EVP.CTRL_CCM_SET_TAG, len(tag), tag) + evp.decrypt_init(key=key, iv=nonce) + evp.decrypt_update(None, None, len(txt)) + evp.decrypt_update(None, aad, len(aad)) + return evp.decrypt_update(len(txt), txt, len(txt)) + + +class OpenSSLWrapper: + NID_X9_62_prime256v1 = 415 # NIST Curve P-256 + + def __init__(self, libcrypto): + self.crypto = ctypes.CDLL(libcrypto) + self.crypto.BN_new.restype = c_void_p + self.crypto.BN_num_bits.restype = c_int + self.crypto.BN_bn2bin.restype = c_int + self.crypto.BN_bin2bn.restype = c_void_p + self.crypto.BN_free.restype = None + self.crypto.RAND_bytes.restype = c_int + self.crypto.EC_KEY_new_by_curve_name.restype = c_void_p + self.crypto.EC_KEY_generate_key.restype = c_int + self.crypto.EC_KEY_check_key.restype = c_int + self.crypto.EC_KEY_set_public_key.restype = c_int + self.crypto.EC_KEY_set_public_key_affine_coordinates.restype = c_int + self.crypto.EC_KEY_get0_public_key.restype = c_void_p + self.crypto.EC_KEY_get0_group.restype = c_void_p + self.crypto.EC_KEY_free.restype = None + self.crypto.EC_POINT_new.restype = c_void_p + self.crypto.EC_POINT_get_affine_coordinates_GFp.restype = c_int + self.crypto.EC_POINT_set_affine_coordinates_GFp.restype = c_int + self.crypto.EC_POINT_free.restype = None + self.crypto.ECDH_OpenSSL.restype = c_void_p + self.crypto.ECDH_set_method.restype = c_int + self.crypto.ECDH_compute_key.restype = c_int + self.crypto.CMAC_CTX_new.restype = c_void_p + self.crypto.CMAC_CTX_free.restype = None + self.crypto.CMAC_Init.restype = c_int + self.crypto.CMAC_Update.restype = c_int + self.crypto.CMAC_Final.restype = c_int + + self.crypto.EVP_CIPHER_CTX_new.restype = c_void_p + self.crypto.EVP_CIPHER_CTX_init.restype = None + self.crypto.EVP_CIPHER_CTX_ctrl.restype = c_int + self.crypto.EVP_CIPHER_CTX_free.restype = None + + self.crypto.EVP_EncryptInit_ex.restype = c_int + self.crypto.EVP_EncryptUpdate.restype = c_int + self.crypto.EVP_EncryptFinal.restype = c_int + self.crypto.EVP_DecryptInit_ex.restype = c_int + self.crypto.EVP_DecryptUpdate.restype = c_int + self.crypto.EVP_DecryptFinal.restype = c_int + + self.crypto.EVP_aes_128_cbc.restype = c_void_p + self.crypto.EVP_aes_128_cbc.argtypes = [] + self.crypto.EVP_aes_128_ccm.restype = c_void_p + self.crypto.EVP_aes_128_ccm.argtypes = [] + + self.EVP_aes_128_cbc = self.crypto.EVP_aes_128_cbc + self.EVP_aes_128_ccm = self.crypto.EVP_aes_128_ccm + + class BIGNUM: + def __init__(self, bignum, release=False): + self._bignum = bignum + self._release = release + + def __del__(self): + if self._release: + OpenSSL.crypto.BN_free(self) + + @property + def _as_parameter_(self): + return c_void_p(self._bignum) + + @staticmethod + def new(): + # BIGNUM *BN_new(void); + bignum = OpenSSL.crypto.BN_new() + if bignum is None: + log.error("BN_new") + else: + return OpenSSL.BIGNUM(bignum, release=True) + + def num_bits(self): + return OpenSSL.crypto.BN_num_bits(self) + + def num_bytes(self): + return (self.num_bits() + 7) // 8 + + def bn2bin(self, num_bytes=None): + # int BN_bn2bin(const BIGNUM *a, unsigned char *to); + if num_bytes is None: + num_bytes = self.num_bytes() + else: + assert num_bytes >= self.num_bytes(), "bn2bin num bytes" + strbuf = ctypes.create_string_buffer(num_bytes) + OpenSSL.crypto.BN_bn2bin(self, strbuf) + return strbuf.raw + + @staticmethod + def bin2bn(s): + # BIGNUM *BN_bin2bn(const unsigned char *s, int len, BIGNUM *ret); + strbuf = ctypes.create_string_buffer(bytes(s), len(s)) + res = OpenSSL.crypto.BN_bin2bn(strbuf, len(s), None) + if res is None: + log.error("BN_bin2bn") + else: + return OpenSSL.BIGNUM(res) + + def rand_bytes(self, num): + # int RAND_bytes(unsigned char *buf, int num); + buf = ctypes.create_string_buffer(num) + res = self.crypto.RAND_bytes(buf, c_int(num)) + if res == 0: + log.error("RAND_bytes") + else: + return buf.raw + + class EC_KEY: + def __init__(self, ec_key): + self._ec_key = ec_key + + def __del__(self): + OpenSSL.crypto.EC_KEY_free(self) + + @property + def _as_parameter_(self): + return c_void_p(self._ec_key) + + @staticmethod + def new_by_curve_name(nid): + # EC_KEY *EC_KEY_new_by_curve_name(int nid); + res = OpenSSL.crypto.EC_KEY_new_by_curve_name(c_int(nid)) + if res is None: + log.error("EC_KEY_new_by_curve_name") + else: + return OpenSSL.EC_KEY(res) + + def generate_key(self): + # int EC_KEY_generate_key(EC_KEY *key); + res = OpenSSL.crypto.EC_KEY_generate_key(self) + if res == 0: + log.error("EC_KEY_generate_key") + return bool(res) + + def check_key(self): + # int EC_KEY_check_key(const EC_KEY *key); + res = OpenSSL.crypto.EC_KEY_check_key(self) + if res == 0: + log.error("EC_KEY_check_key") + return bool(res) + + def set_public_key_affine_coordinates(self, pubkey_x, pubkey_y): + # int EC_KEY_set_public_key_affine_coordinates(EC_KEY *key, + # BIGNUM *x, BIGNUM *y); + r = OpenSSL.crypto.EC_KEY_set_public_key_affine_coordinates( + self, *list(map(OpenSSL.BIGNUM.bin2bn, (pubkey_x, pubkey_y)))) + if r != 1: + errmsg = "EC_KEY_set_public_key_affine_coordinates" + raise AssertionError(errmsg) + + def get_public_key(self): + # const EC_POINT *EC_KEY_get0_public_key(const EC_KEY *key); + res = OpenSSL.crypto.EC_KEY_get0_public_key(self) + if res is None: + log.error("EC_KEY_get0_public_key") + else: + return OpenSSL.EC_POINT(res) + + def get_group(self): + # const EC_GROUP *EC_KEY_get0_group(const EC_KEY *key); + res = OpenSSL.crypto.EC_KEY_get0_group(self) + if res is None: + log.error("EC_KEY_get0_group") + else: + return OpenSSL.EC_GROUP(res) + + class EC_GROUP: + def __init__(self, ec_group): + self._ec_group = ec_group + + @property + def _as_parameter_(self): + return c_void_p(self._ec_group) + + class EC_POINT: + def __init__(self, ec_point): + self._ec_point = ec_point + + @property + def _as_parameter_(self): + return c_void_p(self._ec_point) + + def get_affine_coordinates_GFp(self, ec_group): + # int EC_POINT_get_affine_coordinates_GFp(const EC_GROUP *group, + # const EC_POINT *p, BIGNUM *x, BIGNUM *y, BN_CTX *ctx); + x, y = (OpenSSL.BIGNUM.new(), OpenSSL.BIGNUM.new()) + func = OpenSSL.crypto.EC_POINT_get_affine_coordinates_GFp + res = func(ec_group, self, x, y, None) + if res == 0: + log.error("EC_POINT_get_affine_coordinates_GFp") + else: + return (x.bn2bin(32), y.bn2bin(32)) + + class ECDH: + def __init__(self, local_key): + self.key = local_key + method = OpenSSL.crypto.ECDH_OpenSSL() + OpenSSL.crypto.ECDH_set_method(self.key, c_void_p(method)) + + def compute_key(self, pub_key): + # int ECDH_compute_key(void *out, size_t outlen, + # const EC_POINT *pub_key, EC_KEY *ecdh, + # void *(*KDF)(const void *in, size_t inlen, + # void *out, size_t *outlen)); + strbuf = ctypes.create_string_buffer(32) + args = (strbuf, 32, pub_key, self.key, None) + r = OpenSSL.crypto.ECDH_compute_key(*args) + assert r == 32, "ECDH_compute_key" + return strbuf.raw # the shared secret z + + class CMAC: + def __init__(self, cipher): + # CMAC_CTX *CMAC_CTX_new(void); + self._cmac_ctx = OpenSSL.crypto.CMAC_CTX_new() + self._cipher = cipher + + def __del__(self): + # void CMAC_CTX_free(CMAC_CTX *ctx); + OpenSSL.crypto.CMAC_CTX_free(self) + + @property + def _as_parameter_(self): + return c_void_p(self._cmac_ctx) + + def init(self, key): + # int CMAC_Init(CMAC_CTX *ctx, const void *key, size_t keylen, + # const EVP_CIPHER *cipher, ENGINE *impl); + assert len(key) == 16 + keybuf = ctypes.create_string_buffer(key, 16) + keylen = ctypes.c_size_t(16) + cipher = ctypes.c_void_p(self._cipher) + r = OpenSSL.crypto.CMAC_Init(self, keybuf, keylen, cipher, None) + assert r == 1, "CMAC_Init" + return self + + def update(self, msg): + # int CMAC_Update(CMAC_CTX *ctx, const void *data, size_t dlen); + msgbuf = ctypes.create_string_buffer(msg, len(msg)) + msglen = ctypes.c_size_t(len(msg)) + r = OpenSSL.crypto.CMAC_Update(self, msgbuf, msglen) + assert r == 1, "CMAC_Update" + return self + + def final(self): + macbuf = ctypes.create_string_buffer(16) + maclen = ctypes.c_size_t(0) + rc = OpenSSL.crypto.CMAC_Final(self, macbuf, ctypes.byref(maclen)) + assert rc == 1 and maclen.value == 16, "CMAC_Final" + return macbuf.raw + + class EVP: + CTRL_CCM_SET_IVLEN = 0x09 + CTRL_CCM_GET_TAG = 0x10 + CTRL_CCM_SET_TAG = 0x11 + CTRL_CCM_SET_L = 0x14 + + class CIPHER_CTX: + def __init__(self): + # EVP_CIPHER_CTX *EVP_CIPHER_CTX_new(void); + ctx = OpenSSL.crypto.EVP_CIPHER_CTX_new() + if ctx is None: + raise AssertionError("EVP_CIPHER_CTX_new") + self._ctx = ctx + + def __del__(self): + # void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *ctx); + OpenSSL.crypto.EVP_CIPHER_CTX_free(self) + + @property + def _as_parameter_(self): + return c_void_p(self._ctx) + + def ctrl_set(self, op, arg, ptr=None): + # int EVP_CIPHER_CTX_ctrl(EVP_CIPHER_CTX *ctx, int type, + # int arg, void *ptr); + r = OpenSSL.crypto.EVP_CIPHER_CTX_ctrl(self, op, arg, ptr) + if r != 1: + raise AssertionError("EVP_CIPHER_CTX_ctrl") + + def ctrl_get(self, op, arg): + # int EVP_CIPHER_CTX_ctrl(EVP_CIPHER_CTX *ctx, int type, + # int arg, void *ptr); + outbuf = ctypes.create_string_buffer(arg) + r = OpenSSL.crypto.EVP_CIPHER_CTX_ctrl(self, op, arg, outbuf) + if r != 1: + raise AssertionError("EVP_CIPHER_CTX_ctrl") + return outbuf.raw + + def __init__(self, evp_cipher_ctx=None): + if evp_cipher_ctx: + self._ctx = evp_cipher_ctx + else: + self._ctx = OpenSSL.EVP.CIPHER_CTX() + + @property + def cipher_ctx(self): + return self._ctx + + def encrypt_init(self, evp_cipher=None, key=None, iv=None): + # int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx, + # const EVP_CIPHER *type, ENGINE *impl, + # unsigned char *key, unsigned char *iv); + r = OpenSSL.crypto.EVP_EncryptInit_ex( + self._ctx, c_void_p(evp_cipher), None, key, iv) + if r != 1: + raise AssertionError("EVP_EncryptInit_ex") + + def encrypt_update(self, out_len, message, msg_len): + # int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, + # int *outl, unsigned char *in, int inl); + if out_len is None: + out_buf = None + out_len = c_int(0) + else: + out_buf = ctypes.create_string_buffer(out_len) + out_len = c_int(out_len) + r = OpenSSL.crypto.EVP_EncryptUpdate( + self._ctx, out_buf, ctypes.byref(out_len), message, msg_len) + if r != 1: + raise AssertionError("EVP_EncryptUpdate") + return out_buf.raw[0:out_len.value] if out_buf else b'' + + def decrypt_init(self, evp_cipher=None, key=None, iv=None): + # int EVP_DecryptInit_ex(EVP_CIPHER_CTX *ctx, + # const EVP_CIPHER *type, ENGINE *impl, + # unsigned char *key, unsigned char *iv); + r = OpenSSL.crypto.EVP_DecryptInit_ex( + self._ctx, c_void_p(evp_cipher), None, key, iv) + if r != 1: + raise AssertionError("EVP_DecryptInit_ex") + + def decrypt_update(self, out_len, message, msg_len): + # int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, + # int *outl, unsigned char *in, int inl); + if out_len is None: + out_buf = None + out_len = c_int(0) + else: + out_buf = ctypes.create_string_buffer(out_len) + out_len = c_int(out_len) + r = OpenSSL.crypto.EVP_DecryptUpdate( + self._ctx, out_buf, ctypes.byref(out_len), message, msg_len) + if r != 1: + raise AssertionError("EVP_DecryptUpdate") + return out_buf.raw[0:out_len.value] if out_buf else b'' + + +libcrypto = ctypes.util.find_library('crypto.so.1.0') +if libcrypto is not None: + OpenSSL = OpenSSLWrapper(libcrypto) diff --git a/src/lib/nfc/llcp/socket.py b/src/lib/nfc/llcp/socket.py new file mode 100644 index 0000000..a51d915 --- /dev/null +++ b/src/lib/nfc/llcp/socket.py @@ -0,0 +1,177 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- + + +class Socket(object): + """ + Create a new LLCP socket with the given socket type. The + socket type should be one of: + + * :const:`nfc.llcp.LOGICAL_DATA_LINK` for best-effort + communication using LLCP connection-less PDU exchange + + * :const:`nfc.llcp.DATA_LINK_CONNECTION` for reliable + communication using LLCP connection-mode PDU exchange + + * :const:`nfc.llcp.llc.RAW_ACCESS_POINT` for unregulated LLCP PDU + exchange (useful to implement test programs) + """ + def __init__(self, llc, sock_type): + self._tco = None if sock_type is None else llc.socket(sock_type) + self._llc = llc + + @property + def llc(self): + """The :class:`~nfc.llcp..llc.LogicalLinkController` instance + to which this socket belongs. This attribute is read-only.""" + return self._llc + + def resolve(self, name): + """Resolve a service name into an address. This may involve + conversation with the remote service discovery component if + the name is hasn't yet been resolved. The return value is the + service access point address for the service name bound at the + remote device. The address value 0 indicates that the remote + device does not have a service with the requested name. The + address value 1 indicates that the remote device has a data + link connection service with the requested name that can only + be connected by service name. The return value is None when + communication with the peer device terminated while waiting + for a response. + + """ + return self.llc.resolve(name) + + def setsockopt(self, option, value): + """Set the value of the given socket option and return the + current value which may have been corrected if it was out of + bounds.""" + return self.llc.setsockopt(self._tco, option, value) + + def getsockopt(self, option): + """Return the value of the given socket option.""" + return self.llc.getsockopt(self._tco, option) + + def bind(self, address=None): + """Bind the socket to address. The socket must not already be + bound. The address may be a service name string, a service + access point number, or it may be omitted. If address is a + well-known service name the socket will be bound to the + corresponding service access point address, otherwise the + socket will be bound to the next available service access + point address between 16 and 31 (inclusively). If address is a + number between 32 and 63 (inclusively) the socket will be + bound to that service access point address. If the address + argument is omitted the socket will be bound to the next + available service access point address between 32 and 63.""" + return self.llc.bind(self._tco, address) + + def connect(self, address): + """Connect to a remote socket at address. Address may be a + service name string or a service access point number.""" + return self.llc.connect(self._tco, address) + + def listen(self, backlog): + """Mark a socket as a socket that will be used to accept + incoming connection requests using accept(). The *backlog* + defines the maximum length to which the queue of pending + connections for the socket may grow. A backlog of zero + disables queuing of connection requests. + """ + return self.llc.listen(self._tco, backlog) + + def accept(self): + """Accept a connection. The socket must be bound to an address + and listening for connections. The return value is a new + socket object usable to send and receive data on the + connection.""" + socket = Socket(self._llc, None) + socket._tco = self.llc.accept(self._tco) + return socket + + def send(self, data, flags=0): + """Send data to the socket. The socket must be connected to a remote + socket. Returns a boolean value that indicates success or + failure. A false value is typically an indication that the + socket or connection was closed. + + """ + return self.llc.send(self._tco, data, flags) + + def sendto(self, data, addr, flags=0): + """Send data to the socket. The socket should not be connected + to a remote socket, since the destination socket is specified + by addr. Returns a boolean value that indicates success + or failure. Failure to send is generally an indication that + the socket was closed.""" + return self.llc.sendto(self._tco, data, addr, flags) + + def recv(self): + """Receive data from the socket. The return value is a bytes object + representing the data received. The maximum amount of data + that may be returned is determined by the link or connection + maximum information unit size.""" + return self.llc.recv(self._tco) + + def recvfrom(self): + """Receive data from the socket. The return value is a pair + (bytes, address) where string is a string representing the + data received and address is the address of the socket sending + the data.""" + return self.llc.recvfrom(self._tco) + + def poll(self, event, timeout=None): + """Wait for a socket event. Posssible *event* values are the strings + "recv", "send" and "acks". Whent the timeout is present and + not :const:`None`, it should be a floating point number + specifying the timeout for the operation in seconds (or + fractions thereof). For "recv" or "send" the :meth:`poll` + method returns :const:`True` if a next :meth:`recv` or + :meth:`send` operation would be non-blocking. The "acks" event + may only be used with a data-link-connection type socket; the + call then returns :const:`True` if the counter of received + acknowledgements was greater than zero and decrements the + counter by one. + + """ + return self.llc.poll(self._tco, event, timeout) + + def getsockname(self): + """Obtain the address to which the socket is bound. For an + unbound socket the returned value is None. + """ + return self.llc.getsockname(self._tco) + + def getpeername(self): + """Obtain the address of the peer connected on the socket. For + an unconnected socket the returned value is None. + """ + return self.llc.getpeername(self._tco) + + def close(self): + """Close the socket. All future operations on the socket + object will fail. The remote end will receive no more data + Sockets are automatically closed when the logical link + controller terminates (gracefully or by link disruption). A + connection-mode socket will attempt to disconnect the data + link connection (if in connected state).""" + return self.llc.close(self._tco) diff --git a/src/lib/nfc/llcp/tco.py b/src/lib/nfc/llcp/tco.py new file mode 100644 index 0000000..3c049eb --- /dev/null +++ b/src/lib/nfc/llcp/tco.py @@ -0,0 +1,733 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +from . import pdu +from . import err +import src.lib.nfc.llcp + +import errno +import threading +import collections + +import logging +log = logging.getLogger(__name__) + + +class TransmissionControlObject(object): + class State(object): + def __init__(self): + self.names = ("SHUTDOWN", "CLOSED", "LISTEN", "CONNECT", + "ESTABLISHED", "DISCONNECT", "CLOSE_WAIT") + self.value = self.names.index("SHUTDOWN") + + def __str__(self): + return self.names[self.value] + + def __getattr__(self, name): + return self.value == self.names.index(name) + + def __setattr__(self, name, value): + if name not in ("names", "value"): + value, name = self.names.index(name), "value" + object.__setattr__(self, name, value) + + class Mode(object): + def __init__(self): + self.names = ("BLOCK", "SEND_BUSY", "RECV_BUSY", "RECV_BUSY_SENT") + self.value = dict([(name, False) for name in self.names]) + + def __str__(self): + return str(self.value) + + def __getattr__(self, name): + return self.value[name] + + def __init__(self, send_miu, recv_miu): + self.lock = threading.RLock() + self.mode = TransmissionControlObject.Mode() + self.state = TransmissionControlObject.State() + self.send_queue = collections.deque() + self.recv_queue = collections.deque() + self.send_ready = threading.Condition(self.lock) + self.recv_ready = threading.Condition(self.lock) + self.recv_miu = recv_miu + self.send_miu = send_miu + self.recv_buf = 1 + self.send_buf = 1 + self.addr = None + self.peer = None + + @property + def is_bound(self): + return self.addr is not None + + def setsockopt(self, option, value): + if option == nfc.llcp.SO_SNDBUF: + # with self.lock: self.send_buf = int(value) + # adjustable send buffer only with non-blocking socket mode + raise NotImplementedError("SO_SNDBUF can not be set") + elif option == nfc.llcp.SO_RCVBUF: + with self.lock: + self.recv_buf = int(value) + else: + raise ValueError("invalid option value") + + def getsockopt(self, option): + if option == nfc.llcp.SO_SNDMIU: + return self.send_miu + if option == nfc.llcp.SO_RCVMIU: + return self.recv_miu + if option == nfc.llcp.SO_SNDBUF: + return self.send_buf + if option == nfc.llcp.SO_RCVBUF: + return self.recv_buf + + def bind(self, addr): + if self.addr and addr and self.addr != addr: + log.warning("socket rebound from {} to {}".format(self.addr, addr)) + self.addr = addr + return self.addr + + def poll(self, event, timeout): + if event == "recv": + with self.recv_ready: + if len(self.recv_queue) == 0: + self.recv_ready.wait(timeout) + if len(self.recv_queue) > 0: + return self.recv_queue[0] + return None + if event == "send": + with self.send_ready: + if len(self.send_queue) >= self.send_buf: + self.send_ready.wait(timeout) + return len(self.send_queue) < self.send_buf + + def send(self, send_pdu, flags): + with self.send_ready: + self.send_queue.append(send_pdu) + if not (flags & nfc.llcp.MSG_DONTWAIT): + self.send_ready.wait() + + def recv(self): + with self.recv_ready: + try: + return self.recv_queue.popleft() + except IndexError: + self.recv_ready.wait() + return self.recv_queue.popleft() + + def close(self): + with self.lock: + self.send_queue.clear() + self.recv_queue.clear() + self.send_ready.notify_all() + self.recv_ready.notify_all() + self.state.SHUTDOWN = True + + # + # enqueue() and dequeue() are called from llc run thread + # + def enqueue(self, rcvd_pdu): + with self.lock: + if len(self.recv_queue) < self.recv_buf: + log.debug("enqueue {0}".format(rcvd_pdu)) + self.recv_queue.append(rcvd_pdu) + self.recv_ready.notify() + return True + else: + log.warning("discard {0}".format(rcvd_pdu)) + return False + + def dequeue(self, miu_size, icv_size, notify=True): + # Return the first pending outbound PDU if it's information + # field size (total size - header size) does not exceed the + # given miu_size value. For UI and I PDUs do also consider the + # icv_size value (this is set to non-zero by the packet + # collector when aggregating). Re-insert the PDU at the + # beginning of the send queue if it exceeds the miu_size. + # Skip the length check if miu_size is None. + with self.lock: + try: + send_pdu = self.send_queue.popleft() + log.debug("dequeue {0}".format(send_pdu)) + except IndexError: + return None + + if send_pdu.name in ("UI", "I"): + pdu_size = len(send_pdu) + icv_size + else: + pdu_size = len(send_pdu) + + if ((miu_size is not None and + pdu_size - send_pdu.header_size > miu_size)): + log.debug("requeue {0}".format(send_pdu)) + self.send_queue.appendleft(send_pdu) + return None + + if notify is True: + self.send_ready.notify() + + return send_pdu + + +class RawAccessPoint(TransmissionControlObject): + """ + ============= =========== ============ + State Event Transition + ============= =========== ============ + SHUTDOWN init() ESTABLISHED + ESTABLISHED close() SHUTDOWN + ============= =========== ============ + """ + def __init__(self, recv_miu): + super(RawAccessPoint, self).__init__(128, recv_miu) + self.state.ESTABLISHED = True + + def __str__(self): + return "RAW {:2} -> ?".format(self.addr + if self.addr is not None + else "None") + + def setsockopt(self, option, value): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + super(RawAccessPoint, self).setsockopt(option, value) + + def getsockopt(self, option): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + return super(RawAccessPoint, self).getsockopt(option) + + def poll(self, event, timeout): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + if event not in ("recv", "send"): + raise err.Error(errno.EINVAL) + return super(RawAccessPoint, self).poll(event, timeout) is not None + + def send(self, send_pdu, flags): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + log.debug("{0} send {1}".format(str(self), send_pdu)) + super(RawAccessPoint, self).send(send_pdu, flags) + return self.state.ESTABLISHED is True + + def recv(self): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + try: + return super(RawAccessPoint, self).recv() + except IndexError: + raise err.Error(errno.EPIPE) + + def close(self): + super(RawAccessPoint, self).close() + + # + # enqueue() and dequeue() are called from llc run thread + # + def enqueue(self, rcvd_pdu): + return super(RawAccessPoint, self).enqueue(rcvd_pdu) + + def dequeue(self, miu_size, icv_size): + return super(RawAccessPoint, self).dequeue(miu_size=None, icv_size=0) + + +class LogicalDataLink(TransmissionControlObject): + """ + ============= =========== ============ + State Event Transition + ============= =========== ============ + SHUTDOWN init() ESTABLISHED + ESTABLISHED close() SHUTDOWN + ============= =========== ============ + """ + def __init__(self, recv_miu): + super(LogicalDataLink, self).__init__(128, recv_miu) + self.state.ESTABLISHED = True + + def __str__(self): + return "LDL {addr:2} -> {peer:2}".format( + addr=self.addr if self.addr is not None else "None", + peer=self.peer if self.peer is not None else "None" + ) + + def setsockopt(self, option, value): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + super(LogicalDataLink, self).setsockopt(option, value) + + def getsockopt(self, option): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + return super(LogicalDataLink, self).getsockopt(option) + + def connect(self, dest): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + with self.lock: + self.peer = dest + return self.peer > 0 + + def poll(self, event, timeout): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + if event not in ("recv", "send"): + raise err.Error(errno.EINVAL) + return super(LogicalDataLink, self).poll(event, timeout) is not None + + def sendto(self, message, dest, flags): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + if self.peer and dest != self.peer: + raise err.Error(errno.EDESTADDRREQ) + if len(message) > self.send_miu: + raise err.Error(errno.EMSGSIZE) + send_pdu = pdu.UnnumberedInformation(dest, self.addr, data=message) + super(LogicalDataLink, self).send(send_pdu, flags) + return self.state.ESTABLISHED is True + + def recvfrom(self): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + try: + rcvd_pdu = super(LogicalDataLink, self).recv() + except IndexError: + raise err.Error(errno.EPIPE) + return (rcvd_pdu.data, rcvd_pdu.ssap) if rcvd_pdu else (None, None) + + def close(self): + super(LogicalDataLink, self).close() + + # + # enqueue() and dequeue() are called from llc run thread + # + def enqueue(self, rcvd_pdu): + if not rcvd_pdu.name == "UI": + log.warning("ignore %s PDU on logical data link", rcvd_pdu.name) + return False + if len(rcvd_pdu.data) > self.recv_miu: + log.warning("received UI PDU exceeds local link MIU") + return False + return super(LogicalDataLink, self).enqueue(rcvd_pdu) + + def dequeue(self, miu_size, icv_size): + return super(LogicalDataLink, self).dequeue(miu_size, icv_size) + + +class DataLinkConnection(TransmissionControlObject): + """ + ============= =========== ============ + State Event Transition + ============= =========== ============ + SHUTDOWN init() ESTABLISHED + CLOSED listen() LISTEN + CLOSED connect() CONNECT + CONNECT CC-PDU ESTABLISHED + CONNECT DM-PDU CLOSED + ESTABLISHED I-PDU ESTABLISHED + ESTABLISHED RR-PDU ESTABLISHED + ESTABLISHED RNR-PDU ESTABLISHED + ESTABLISHED FRMR-PDU SHUTDOWN + ESTABLISHED DISC-PDU CLOSE_WAIT + ESTABLISHED close() SHUTDOWN + CLOSE_WAIT close() SHUTDOWN + ============= =========== ============ + """ + + DLC_PDU_NAMES = ("CONNECT", "DISC", "CC", "DM", "FRMR", "I", "RR", "RNR") + + def __init__(self, recv_miu, recv_win): + super(DataLinkConnection, self).__init__(128, recv_miu) + self.state.CLOSED = True + self.acks_ready = threading.Condition(self.lock) + self.acks_recvd = 0 # received acknowledgements + self.recv_confs = 0 # outstanding receive confirmations + self.send_token = threading.Condition(self.lock) + self.recv_buf = recv_win + self.recv_win = recv_win # RW(Local) + self.recv_cnt = 0 # V(R) + self.recv_ack = 0 # V(RA) + self.send_win = None # RW(Remote) + self.send_cnt = 0 # V(S) + self.send_ack = 0 # V(SA) + + def __str__(self): + s = "DLC {addr:2} <-> {peer:2} {dlc.state} " + s += "RW(R)={dlc.send_win} V(S)={dlc.send_cnt} V(SA)={dlc.send_ack} " + s += "RW(L)={dlc.recv_win} V(R)={dlc.recv_cnt} V(RA)={dlc.recv_ack}" + return s.format( + dlc=self, + addr=self.addr if self.addr is not None else "None", + peer=self.peer if self.peer is not None else "None" + ) + + def log(self, string): + log.debug("DLC ({dlc.addr},{dlc.peer}) {dlc.state} {s}" + .format(dlc=self, s=string)) + + def err(self, string): + log.error("DLC ({dlc.addr},{dlc.peer}) {s}".format(dlc=self, s=string)) + + def setsockopt(self, option, value): + with self.lock: + if option == nfc.llcp.SO_RCVMIU and self.state.CLOSED: + self.recv_miu = min(value, 2175) + return + if option == nfc.llcp.SO_RCVBUF and self.state.CLOSED: + self.recv_win = min(value, 15) + self.recv_buf = self.recv_win + return + if option == nfc.llcp.SO_RCVBSY: + self.mode.RECV_BUSY = bool(value) + return + super(DataLinkConnection, self).setsockopt(option, value) + + def getsockopt(self, option): + if option == nfc.llcp.SO_RCVBUF: + return self.recv_win + if option == nfc.llcp.SO_SNDBSY: + return self.mode.SEND_BUSY + if option == nfc.llcp.SO_RCVBSY: + return self.mode.RECV_BUSY + return super(DataLinkConnection, self).getsockopt(option) + + def listen(self, backlog): + with self.lock: + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + if not self.state.CLOSED: + self.err("listen() but socket state is {0}".format(self.state)) + raise err.Error(errno.ENOTSUP) + self.state.LISTEN = True + self.recv_buf = backlog + + def accept(self): + with self.lock: + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + if not self.state.LISTEN: + self.err("accept() but socket state is {0}".format(self.state)) + raise err.Error(errno.EINVAL) + self.recv_buf += 1 + try: + rcvd_pdu = super(DataLinkConnection, self).recv() + except IndexError: + raise err.Error(errno.EPIPE) + self.recv_buf -= 1 + if rcvd_pdu.name == "CONNECT": + dlc = DataLinkConnection(self.recv_miu, self.recv_win) + dlc.addr = self.addr + dlc.peer = rcvd_pdu.ssap + dlc.send_miu = rcvd_pdu.miu + dlc.send_win = rcvd_pdu.rw + send_pdu = pdu.ConnectionComplete(dlc.peer, dlc.addr) + send_pdu.miu, send_pdu.rw = dlc.recv_miu, dlc.recv_win + log.debug("accepting CONNECT from SAP %d" % dlc.peer) + dlc.state.ESTABLISHED = True + self.send_queue.append(send_pdu) + return dlc + else: # pragma: no cover + raise RuntimeError("CONNECT expected, not " + rcvd_pdu.name) + + def connect(self, dest): + with self.lock: + if not self.state.CLOSED: + self.err("connect() in socket state {0}".format(self.state)) + if self.state.ESTABLISHED: + raise err.Error(errno.EISCONN) + if self.state.CONNECT: + raise err.Error(errno.EALREADY) + raise err.Error(errno.EPIPE) + if isinstance(dest, (bytes, bytearray)): + send_pdu = pdu.Connect(1, self.addr, self.recv_miu, + self.recv_win, bytes(dest)) + elif isinstance(dest, str): + send_pdu = pdu.Connect(1, self.addr, self.recv_miu, + self.recv_win, dest.encode('latin')) + elif isinstance(dest, int): + send_pdu = pdu.Connect(dest, self.addr, self.recv_miu, + self.recv_win) + else: + raise TypeError("connect destination must be int or bytes") + + self.state.CONNECT = True + self.send_queue.append(send_pdu) + + try: + rcvd_pdu = super(DataLinkConnection, self).recv() + except IndexError: + raise err.Error(errno.EPIPE) + + if rcvd_pdu.name == "DM": + logstr = "connect rejected with reason {}" + self.log(logstr.format(rcvd_pdu.reason)) + self.state.CLOSED = True + raise err.ConnectRefused(rcvd_pdu.reason) + elif rcvd_pdu.name == "CC": + self.peer = rcvd_pdu.ssap + self.recv_buf = self.recv_win + self.send_miu = rcvd_pdu.miu + self.send_win = rcvd_pdu.rw + self.state.ESTABLISHED = True + return + else: # pragma: no cover + raise RuntimeError("CC or DM expected, not " + rcvd_pdu.name) + + @property + def send_window_slots(self): + # RW(R) - V(S) + V(SA) mod 16 + return (self.send_win - self.send_cnt + self.send_ack) % 16 + + @property + def recv_window_slots(self): + # RW(L) - V(R) + V(RA) mod 16 + return (self.recv_win - self.recv_cnt + self.recv_ack) % 16 + + def send(self, message, flags): + with self.send_token: + if not self.state.ESTABLISHED: + self.err("send() in socket state {0}".format(self.state)) + if self.state.CLOSE_WAIT: + raise err.Error(errno.EPIPE) + raise err.Error(errno.ENOTCONN) + if len(message) > self.send_miu: + raise err.Error(errno.EMSGSIZE) + while self.send_window_slots == 0 and self.state.ESTABLISHED: + if flags & nfc.llcp.MSG_DONTWAIT: + raise err.Error(errno.EWOULDBLOCK) + self.log("waiting on busy send window") + self.send_token.wait() + self.log("send {0} byte on {1}".format(len(message), str(self))) + if self.state.ESTABLISHED: + send_pdu = pdu.Information(self.peer, self.addr, data=message) + send_pdu.ns = self.send_cnt + self.send_cnt = (self.send_cnt + 1) % 16 + super(DataLinkConnection, self).send(send_pdu, flags) + return self.state.ESTABLISHED is True + + def recv(self): + with self.lock: + if not (self.state.ESTABLISHED or self.state.CLOSE_WAIT): + self.err("recv() in socket state {0}".format(self.state)) + raise err.Error(errno.ENOTCONN) + + try: + rcvd_pdu = super(DataLinkConnection, self).recv() + except IndexError: + return None + + if rcvd_pdu.name == "I": + self.recv_confs += 1 + if self.recv_confs > self.recv_win: + self.err("recv_confs({0}) > recv_win({1})" + .format(self.recv_confs, self.recv_win)) + raise RuntimeError("recv_confs > recv_win") + return rcvd_pdu.data + + if rcvd_pdu.name == "DISC": + self.close() + return None + + raise RuntimeError("only I or DISC expected, not " + rcvd_pdu.name) + + def poll(self, event, timeout): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + + if event == "recv": + if self.state.ESTABLISHED or self.state.CLOSE_WAIT: + rcvd_pdu = super(DataLinkConnection, self).poll(event, timeout) + if self.state.ESTABLISHED or self.state.CLOSE_WAIT: + return isinstance(rcvd_pdu, pdu.Information) + elif event == "send": + if self.state.ESTABLISHED: + if super(DataLinkConnection, self).poll(event, timeout): + return self.state.ESTABLISHED + return False + elif event == "acks": + with self.acks_ready: + if not self.acks_recvd > 0: + self.acks_ready.wait(timeout) + if self.acks_recvd > 0: + self.acks_recvd = self.acks_recvd - 1 + return True + return False + else: + raise err.Error(errno.EINVAL) + + def close(self): + with self.lock: + self.log("close()") + if self.state.ESTABLISHED and self.is_bound: + self.state.DISCONNECT = True + self.send_token.notify_all() + self.acks_ready.notify_all() + send_pdu = pdu.Disconnect(self.peer, self.addr) + self.send_queue.append(send_pdu) + try: + super(DataLinkConnection, self).recv() + except IndexError: + pass + super(DataLinkConnection, self).close() + self.acks_ready.notify_all() + self.send_token.notify_all() + + # + # enqueue() and dequeue() are called from llc thread context + # + def enqueue(self, rcvd_pdu): + self.log("enqueue {pdu.name} PDU".format(pdu=rcvd_pdu)) + + if rcvd_pdu.name not in self.DLC_PDU_NAMES: + self.err("non connection mode pdu on data link connection") + send_pdu = pdu.FrameReject.from_pdu(rcvd_pdu, flags="W", dlc=self) + self.close() + self.send_queue.append(send_pdu) + return + + if self.state.CLOSED: + self.send_queue.append(pdu.DisconnectedMode( + rcvd_pdu.ssap, rcvd_pdu.dsap, reason=1)) + + elif self.state.LISTEN and rcvd_pdu.name == "CONNECT": + if super(DataLinkConnection, self).enqueue(rcvd_pdu) is False: + log.warning("full backlog on listening socket") + self.send_queue.append(pdu.DisconnectedMode( + rcvd_pdu.ssap, rcvd_pdu.dsap, reason=0x20)) + + elif self.state.CONNECT and rcvd_pdu.name in ("CC", "DM"): + with self.lock: + self.recv_queue.append(rcvd_pdu) + self.recv_ready.notify() + + elif self.state.DISCONNECT and rcvd_pdu.name == "DM": + with self.lock: + self.recv_queue.append(rcvd_pdu) + self.recv_ready.notify() + + elif self.state.ESTABLISHED: + return self._enqueue_state_established(rcvd_pdu) + + def _enqueue_state_established(self, rcvd_pdu): + if rcvd_pdu.name == "I": + frmr = None + if len(rcvd_pdu.data) > self.recv_miu: + frmr = pdu.FrameReject.from_pdu(rcvd_pdu, flags="I", dlc=self) + elif rcvd_pdu.ns != self.recv_cnt: + frmr = pdu.FrameReject.from_pdu(rcvd_pdu, flags="S", dlc=self) + if frmr: + self.log("reject " + str(self)) + self.send_queue.clear() + self.send_queue.append(frmr) + log.debug("enqueued frame reject pdu") + return + + if rcvd_pdu.name == "FRMR": + with self.lock: + self.state.SHUTDOWN = True + self.close() + return + + if rcvd_pdu.name == "DISC": + with self.lock: + self.state.CLOSE_WAIT = True + self.send_queue.clear() + self.send_queue.append(pdu.DisconnectedMode( + self.peer, self.addr, reason=0)) + return + + if rcvd_pdu.name in ("I", "RR", "RNR"): + with self.lock: + # acks = N(R) - V(SA) mod 16 + acks = (rcvd_pdu.nr - self.send_ack) % 16 + if acks: + self.acks_recvd += acks + self.acks_ready.notify_all() + self.send_token.notify() + self.send_ack = rcvd_pdu.nr # V(SA) := N(R) + if rcvd_pdu.name == "RNR": + self.mode.SEND_BUSY = True + if rcvd_pdu.name == "RR": + self.mode.SEND_BUSY = False + + if rcvd_pdu.name == "I": + with self.lock: + # V(R) := V(R) + 1 mod 16 + self.recv_cnt = (self.recv_cnt + 1) % 16 + super(DataLinkConnection, self).enqueue(rcvd_pdu) + + def dequeue(self, miu_size, icv_size): + with self.lock: + if self.state.ESTABLISHED: + if self.mode.RECV_BUSY_SENT != self.mode.RECV_BUSY: + self.mode.RECV_BUSY_SENT = self.mode.RECV_BUSY + ACK = RNR_PDU if self.mode.RECV_BUSY else RR_PDU + return ACK(self.peer, self.addr, self.recv_ack) + + send_pdu = super(DataLinkConnection, self).dequeue( + miu_size, icv_size, notify=False) + + if send_pdu: + self.log("dequeue {0} PDU".format(send_pdu.name)) + + if send_pdu.name == "FRMR": + self.state.SHUTDOWN = True + self.close() + + if send_pdu.name == "I" and self.state.ESTABLISHED: + if self.recv_confs and self.recv_cnt != self.recv_ack: + self.log("piggyback ack " + str(self)) + self.recv_ack = (self.recv_ack + self.recv_confs) % 16 + self.recv_confs = 0 + send_pdu.nr = self.recv_ack + self.send_ready.notify() + + if send_pdu.name == "DM" and self.state.CLOSE_WAIT: + self.recv_queue.append(pdu.Disconnect( + dsap=self.peer, ssap=self.addr)) + self.recv_ready.notify() + self.send_token.notify_all() + + else: + if ((self.state.ESTABLISHED and self.recv_confs + and self.recv_window_slots == 0)): + # must send acknowledgement to keep going + self.log("necessary ack " + str(self)) + self.recv_ack = (self.recv_ack + self.recv_confs) % 16 + self.recv_confs = 0 + ACK = RNR_PDU if self.mode.RECV_BUSY else RR_PDU + return ACK(self.peer, self.addr, self.recv_ack) + + return send_pdu + + def sendack(self): + if self.state.ESTABLISHED: + with self.lock: + if self.recv_confs and self.recv_cnt != self.recv_ack: + self.log("voluntary ack " + str(self)) + self.recv_ack = (self.recv_ack + self.recv_confs) % 16 + self.recv_confs = 0 + ACK = RNR_PDU if self.mode.RECV_BUSY else RR_PDU + return ACK(self.peer, self.addr, self.recv_ack) + + +RR_PDU, RNR_PDU = pdu.ReceiveReady, pdu.ReceiveNotReady diff --git a/src/lib/nfc/snep/__init__.py b/src/lib/nfc/snep/__init__.py new file mode 100644 index 0000000..9e2145c --- /dev/null +++ b/src/lib/nfc/snep/__init__.py @@ -0,0 +1,36 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +""" +The nfc.snep module implements the NFC Forum Simple NDEF Exchange +Protocol (SNEP) specification and provides a server and client class +for applications to easily send or receive SNEP messages. +""" +from src.lib.nfc.snep.server import SnepServer # noqa: F401 +from src.lib.nfc.snep.client import SnepClient # noqa: F401 +from src.lib.nfc.snep.client import SnepError # noqa: F401 + +Success = 0x81 +NotFound = 0xC0 +ExcessData = 0xC1 +BadRequest = 0xC2 +NotImplemented = 0xE0 +UnsupportedVersion = 0xE1 diff --git a/src/lib/nfc/snep/client.py b/src/lib/nfc/snep/client.py new file mode 100644 index 0000000..73a2ade --- /dev/null +++ b/src/lib/nfc/snep/client.py @@ -0,0 +1,247 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +# +# Simple NDEF Exchange Protocol (SNEP) - Client Base Class +# +import ndef +import struct +import src.lib.nfc.llcp + +import logging +log = logging.getLogger(__name__) + + +def send_request(socket, snep_request, send_miu): + if len(snep_request) <= send_miu: + return socket.send(snep_request) + + if not socket.send(snep_request[0:send_miu]): + return False + + if socket.recv() != b"\x10\x80\x00\x00\x00\x00": + return False + + for offset in range(send_miu, len(snep_request), send_miu): + fragment = snep_request[offset:offset+send_miu] + if not socket.send(fragment): + return False + + return True + + +def recv_response(socket, acceptable_length, timeout): + if socket.poll("recv", timeout): + snep_response = socket.recv() + + if len(snep_response) < 6: + log.debug("snep response initial fragment too short") + return None + + version, status, length = struct.unpack(">BBL", snep_response[:6]) + + if length > acceptable_length: + log.debug("snep response exceeds acceptable length") + return None + + if len(snep_response) - 6 < length: + # request remaining fragments + socket.send(b"\x10\x00\x00\x00\x00\x00") + while len(snep_response) - 6 < length: + if socket.poll("recv", timeout): + snep_response += socket.recv() + else: + return None + + return bytearray(snep_response) + + +class SnepClient(object): + """ Simple NDEF exchange protocol - client implementation + """ + def __init__(self, llc, max_ndef_msg_recv_size=1024): + self.acceptable_length = max_ndef_msg_recv_size + self.socket = None + self.llc = llc + + def connect(self, service_name): + """Connect to a SNEP server. This needs only be called to + connect to a server other than the Default SNEP Server at + `urn:nfc:sn:snep` or if the client wants to send multiple + requests with a single connection. + """ + self.close() + self.socket = nfc.llcp.Socket(self.llc, nfc.llcp.DATA_LINK_CONNECTION) + self.socket.connect(service_name) + self.send_miu = self.socket.getsockopt(nfc.llcp.SO_SNDMIU) + + def close(self): + """Close the data link connection with the SNEP server. + """ + if self.socket: + self.socket.close() + self.socket = None + + def get_records(self, records=None, timeout=1.0): + """Get NDEF message records from a SNEP Server. + + .. versionadded:: 0.13 + + The :class:`ndef.Record` list given by *records* is encoded as + the request message octets input to :meth:`get_octets`. The + return value is an :class:`ndef.Record` list decoded from the + response message octets returned by :meth:`get_octets`. Same + as:: + + import ndef + send_octets = ndef.message_encoder(records) + rcvd_octets = snep_client.get_octets(send_octets, timeout) + records = list(ndef.message_decoder(rcvd_octets)) + + """ + octets = b''.join(ndef.message_encoder(records)) if records else None + octets = self.get_octets(octets, timeout) + if octets and len(octets) >= 3: + return list(ndef.message_decoder(octets)) + + def get_octets(self, octets=None, timeout=1.0): + """Get NDEF message octets from a SNEP Server. + + .. versionadded:: 0.13 + + If the client has not yet a data link connection with a SNEP + Server, it temporarily connects to the default SNEP Server, + sends the message octets, disconnects after the server + response, and returns the received message octets. + + """ + if octets is None: + # Send NDEF Message with one empty Record. + octets = b'\xd0\x00\x00' + + if not self.socket: + try: + self.connect('urn:nfc:sn:snep') + except nfc.llcp.ConnectRefused: + return None + else: + self.release_connection = True + else: + self.release_connection = False + + try: + request = struct.pack('>BBLL', 0x10, 0x01, 4 + len(octets), + self.acceptable_length) + octets + + if not send_request(self.socket, request, self.send_miu): + return None + + response = recv_response( + self.socket, self.acceptable_length, timeout) + + if response is not None: + if response[1] != 0x81: + raise SnepError(response[1]) + + return response[6:] + + finally: + if self.release_connection: + self.close() + + def put_records(self, records, timeout=1.0): + """Send NDEF message records to a SNEP Server. + + .. versionadded:: 0.13 + + The :class:`ndef.Record` list given by *records* is encoded + and then send via :meth:`put_octets`. Same as:: + + import ndef + octets = ndef.message_encoder(records) + snep_client.put_octets(octets, timeout) + + """ + octets = b''.join(ndef.message_encoder(records)) + return self.put_octets(octets, timeout) + + def put_octets(self, octets, timeout=1.0): + """Send NDEF message octets to a SNEP Server. + + .. versionadded:: 0.13 + + If the client has not yet a data link connection with a SNEP + Server, it temporarily connects to the default SNEP Server, + sends the message octets and disconnects after the server + response. + + """ + if not self.socket: + try: + self.connect('urn:nfc:sn:snep') + except nfc.llcp.ConnectRefused: + return False + else: + self.release_connection = True + else: + self.release_connection = False + + try: + request = struct.pack('>BBL', 0x10, 0x02, len(octets)) + octets + if not send_request(self.socket, request, self.send_miu): + return False + + response = recv_response(self.socket, 0, timeout) + if response is not None: + if response[1] != 0x81: + raise SnepError(response[1]) + + return True + + finally: + if self.release_connection: + self.close() + + def __enter__(self): + self.connect() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + +class SnepError(Exception): + strerr = {0xC0: "resource not found", + 0xC1: "resource exceeds data size limit", + 0xC2: "malformed request not understood", + 0xE0: "unsupported functionality requested", + 0xE1: "unsupported protocol version"} + + def __init__(self, err): + self.args = (err, SnepError.strerr.get(err, "")) + + def __str__(self): + return "nfc.snep.SnepError: [{errno}] {info}".format( + errno=self.args[0], info=self.args[1]) + + @property + def errno(self): + return self.args[0] diff --git a/src/lib/nfc/snep/server.py b/src/lib/nfc/snep/server.py new file mode 100644 index 0000000..496e437 --- /dev/null +++ b/src/lib/nfc/snep/server.py @@ -0,0 +1,175 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +# +# Simple NDEF Exchange Protocol (SNEP) - Server Base Class +# +import threading +import binascii +import logging +import struct +import errno +import ndef +import src.lib.nfc + + +log = logging.getLogger(__name__) + + +class SnepServer(threading.Thread): + """ NFC Forum Simple NDEF Exchange Protocol server + """ + def __init__(self, llc, service_name="urn:nfc:sn:snep", + max_acceptable_length=0x100000, + recv_miu=1984, recv_buf=15): + + self.max_acceptable_length = min(max_acceptable_length, 0xFFFFFFFF) + socket = nfc.llcp.Socket(llc, nfc.llcp.DATA_LINK_CONNECTION) + recv_miu = socket.setsockopt(nfc.llcp.SO_RCVMIU, recv_miu) + recv_buf = socket.setsockopt(nfc.llcp.SO_RCVBUF, recv_buf) + socket.bind(service_name) + log.info("snep server bound to port {0} (MIU={1}, RW={2}), " + "will accept up to {3} byte NDEF messages" + .format(socket.getsockname(), recv_miu, recv_buf, + self.max_acceptable_length)) + socket.listen(backlog=2) + threading.Thread.__init__(self, name=service_name, + target=self._listen, args=(socket,)) + + def _listen(self, listen_socket): + try: + while True: + client_socket = listen_socket.accept() + client_thread = threading.Thread(target=self._serve, + args=(client_socket,)) + client_thread.start() + except nfc.llcp.Error as error: + (log.debug if error.errno == errno.EPIPE else log.error)(error) + finally: + listen_socket.close() + + def _serve(self, client_socket): + peer_sap = client_socket.getpeername() + log.info("serving snep client on remote sap {0}".format(peer_sap)) + send_miu = client_socket.getsockopt(nfc.llcp.SO_SNDMIU) + try: + while client_socket.poll('recv'): + data = bytearray(client_socket.recv()) + if not data: + break # connection closed + + if len(data) < 6: + log.debug("snep msg initial fragment too short") + break # bail out, this is a bad client + + version, length = struct.unpack_from(">BxL", data) + + if (version >> 4) > 1: + log.debug("unsupported version {0}".format(version >> 4)) + client_socket.send(b"\x10\xE1\x00\x00\x00\x00") + continue + + if length > self.max_acceptable_length: + log.debug("snep msg exceeds max acceptable length") + client_socket.send(b"\x10\xFF\x00\x00\x00\x00") + continue + + if len(data) - 6 < length: + # request remaining fragments + client_socket.send(b"\x10\x80\x00\x00\x00\x00") + while len(data) - 6 < length: + try: + data += client_socket.recv() + except TypeError: + break # connection closed + + # message complete, now handle the request + data = self.process_snep_request(data) + + # send the snep response, fragment if needed + if len(data) <= send_miu: + client_socket.send(data) + else: + client_socket.send(data[0:send_miu]) + if client_socket.recv() == b"\x10\x00\x00\x00\x00\x00": + parts = range(send_miu, len(data), send_miu) + for offset in parts: + client_socket.send(data[offset:offset + send_miu]) + + except nfc.llcp.Error as e: + (log.debug if e.errno == nfc.llcp.errno.EPIPE else log.error)(e) + finally: + client_socket.close() + + def process_snep_request(self, request_data): + assert isinstance(request_data, bytearray) + log.debug("<<< %s", binascii.hexlify(request_data).decode()) + try: + if request_data[1] == 1 and len(request_data) >= 10: + acceptable_length = struct.unpack(">L", request_data[6:10])[0] + octets = request_data[10:] + records = list(ndef.message_decoder(octets, known_types={})) + response = self.process_get_request(records) + if isinstance(response, int): + response_code = response + response_data = b'' + else: + response_code = 0x81 # nfc.snep.Success + response_data = b''.join(ndef.message_encoder(response)) + if len(response_data) > acceptable_length: + response_code = 0xC1 # nfc.snep.ExcessData + response_data = b'' + elif request_data[1] == 2: + octets = request_data[6:] + records = list(ndef.message_decoder(octets, known_types={})) + response_code = self.process_put_request(records) + response_data = b'' + else: + log.debug("bad request (0x{:02x})".format(request_data[1])) + response_code = 0xC2 # nfc.snep.BadRequest + response_data = b'' + except ndef.DecodeError as error: + log.error(repr(error)) + response_code = 0xC2 # nfc.snep.BadRequest + response_data = b'' + except ndef.EncodeError as error: + log.error(repr(error)) + response_code = 0xC0 # nfc.snep.NotFound + response_data = b'' + + header = struct.pack(">BBL", 0x10, response_code, len(response_data)) + response_data = header + response_data + log.debug(">>> %s", binascii.hexlify(response_data).decode()) + return response_data + + def process_get_request(self, ndef_message): + """Handle Get requests. This method should be overwritten by a + subclass of SnepServer to customize it's behavior. The default + implementation simply returns nfc.snep.NotImplemented. + """ + return 0xE0 # NotImplemented + + def process_put_request(self, ndef_message): + """Process a SNEP Put request. This method should be overwritten by a + subclass of SnepServer to customize it's behavior. The default + implementation simply returns nfc.snep.Success. + """ + return 0x81 # nfc.snep.Success diff --git a/src/lib/nfc/tag/__init__.py b/src/lib/nfc/tag/__init__.py new file mode 100644 index 0000000..4cbfbf6 --- /dev/null +++ b/src/lib/nfc/tag/__init__.py @@ -0,0 +1,480 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2013, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import logging +from binascii import hexlify +from ndef import message_decoder, message_encoder + + +logging.captureWarnings(True) +log = logging.getLogger(__name__) + + +class Tag(object): + """The base class for all NFC Tags/Cards. The methods and attributes + defined here are commonly available but some may, depending on the + tag product, also return a :const:`None` value is support is not + available. + + Direct subclasses are the NFC Forum tag types: + :class:`~nfc.tag.tt1.Type1Tag`, :class:`~nfc.tag.tt2.Type2Tag`, + :class:`~nfc.tag.tt3.Type3Tag`, :class:`~nfc.tag.tt4.Type4Tag`. + Some of them are further specialized in vendor/product specific + classes. + + """ + class NDEF(object): + """The NDEF object type that may be read from :attr:`Tag.ndef`. + + This class presents the NDEF management information and the + actual NDEF message by a couple of attributes. It is normally + accessed from a :class:`Tag` instance (further named *tag*) + through the :attr:`Tag.ndef` attribute for reading or writing + NDEF records. :: + + if tag.ndef is not None: + for record in tag.ndef.records: + print(record) + if tag.ndef.is_writeable: + from ndef import TextRecord + tag.ndef.records = [TextRecord("Hello World")] + + """ + def __init__(self, tag): + self._tag = tag + self._data = None + self._capacity = 0 + self._readable = False + self._writeable = False + + def _read_ndef_data(self): + msg = "_read_ndef_data is not implemented for this tag type" + raise NotImplementedError(msg) + + def _write_ndef_data(self, data): + msg = "_write_ndef_data is not implemented for this tag type" + raise NotImplementedError(msg) + + @property + def tag(self): + """A readonly reference to the underlying tag object.""" + return self._tag + + @property + def length(self): + """Length of the current NDEF message in bytes.""" + return len(self._data) if self._data else 0 + + @property + def capacity(self): + """Maximum number of bytes for an NDEF message.""" + return self._capacity + + @property + def is_readable(self): + """:const:`True` if the NDEF data are is readable.""" + return self._readable + + @property + def is_writeable(self): + """:const:`True` if the NDEF data area is writeable.""" + return self._writeable + + @property + def has_changed(self): + """The boolean attribute :attr:`has_changed` allows to determine + whether the NDEF message on the tag is different from the + message that was read or written at an earlier time in the + session. This may for example be the case if the tag is + build to dynamically present different content depending + on some state. + + Note that reading this attribute involves a complete + update of the :class:`Tag.NDEF` instance and it is + possible that :attr:`Tag.ndef` is :const:`None` after the + update (e.g. tag gone during read or a dynamic tag that + failed). A robust implementation should always verify the + value of the :attr:`Tag.ndef` attribute. :: + + if tag.ndef.has_changed and tag.ndef is not None: + for record in tag.ndef.records: + print(record) + + The :attr:`has_changed` attribute can also be used to + verify that NDEF records written to the tag are identical + to the NDEF records stored on the tag. :: + + from ndef import TextRecord + tag.ndef.records = [TextRecord("Hello World")] + if tag.ndef.has_changed: + print("the tag data differs from what was written") + + """ + ndef_data = self._read_ndef_data() + different = self._data != ndef_data + if ndef_data is None: + self._tag._ndef = None + self._data = ndef_data + return different + + @property + def records(self): + """Read or write a list of NDEF Records. + + .. versionadded:: 0.12 + + This attribute is a convinience wrapper for decoding and + encoding of the NDEF message data :attr:`octets`. It uses + the `ndeflib `_ module to + return the list of :class:`ndef.Record` instances decoded + from the NDEF message data or set the message data from a + list of records. :: + + from ndef import TextRecord + if tag.ndef is not None: + for record in tag.ndef.records: + print(record) + try: + tag.ndef.records = [TextRecord('Hello World')] + except nfc.tag.TagCommandError as err: + print("NDEF write failed: " + str(err)) + + Decoding is performed with a relaxed error handling + strategy that ignores minor errors in the NDEF data. The + `ndeflib `_ does also + support 'strict' and 'ignore' error handling which may be + used like so:: + + from ndef import message_decoder, message_encoder + records = message_decoder(tag.ndef.octets, errors='strict') + tag.ndef.octets = b''.join(message_encoder(records)) + + """ + return list(message_decoder(self.octets, errors='relax')) + + @records.setter + def records(self, value): + self.octets = b''.join(message_encoder(value)) + + @property + def octets(self): + """Read or write NDEF message data octets. + + .. versionadded:: 0.12 + + The *octets* attribute returns the NDEF message data + octets as bytes. A bytes or bytearray sequence assigned to + *octets* is immediately written to the NDEF message data + area, unless the Tag memory is write protected or to + small. :: + + if tag.ndef is not None: + print(hexlify(tag.ndef.octets).decode()) + + """ + return bytes(self._data) + + @octets.setter + def octets(self, data): + if not self._writeable: + raise AttributeError("tag ndef area is not writeable") + data = bytearray(data) + if len(data) > self.capacity: + raise ValueError("data length exceeds tag capacity") + self._write_ndef_data(data) + self._data = data + + def __init__(self, clf, target): + self._clf, self._target = (clf, target) + self._ndef = None + self._authenticated = False + + def __str__(self): + """x.__str__() <==> str(x)""" + try: + s = self.type + ' ' + repr(self._product) + except AttributeError: + s = self.type + return "{} ID={}".format(s, hexlify(self.identifier).decode().upper()) + + @property + def clf(self): + return self._clf + + @property + def target(self): + return self._target + + @property + def type(self): + return self.TYPE + + @property + def product(self): + return self._product if hasattr(self, "_product") else self.type + + @property + def identifier(self): + """The unique tag identifier.""" + return bytes(self._nfcid) + + @property + def ndef(self): + """An :class:`NDEF` object if found, otherwise :const:`None`.""" + if self._ndef is None: + ndef = self.NDEF(self) + if ndef.has_changed: + self._ndef = ndef + return self._ndef + + @property + def is_present(self): + """True if the tag is within communication range.""" + return self._is_present() + + @property + def is_authenticated(self): + """True if the tag was successfully authenticated.""" + return bool(self._authenticated) + + def dump(self): + """The dump() method returns a list of strings describing the memory + structure of the tag, suitable for printing with join(). The + list format makes custom indentation a bit easier. :: + + print("\\n".join(["\\t" + line for line in tag.dump()])) + + """ + return [] + + def format(self, version=None, wipe=None): + """Format the tag to make it NDEF compatible or erase content. + + The :meth:`format` method is highly dependent on the tag type, + product and present status, for example a tag that has been + made read-only with lock bits can no longer be formatted or + erased. + + :meth:`format` creates the management information defined by + the NFC Forum to describes the NDEF data area on the tag, this + is also called NDEF mapping. The mapping may differ between + versions of the tag specifications, the mapping to apply can + be specified with the *version* argument as an 8-bit integer + composed of a major version number in the most significant 4 + bit and the minor version number in the least significant 4 + bit. If *version* is not specified then the highest possible + mapping version is used. + + If formatting of the tag is possible, the default behavior of + :meth:`format` is to update only the management information + required to make the tag appear as NDEF compatible and empty, + previously existing data could still be read. If existing data + shall be overwritten, the *wipe* argument can be set to an + 8-bit integer that will be written to all available bytes. + + The :meth:`format` method returns :const:`True` if formatting + was successful, :const:`False` if it failed for some reason, + or :const:`None` if the present tag can not be formatted + either because the tag does not support formatting or it is + not implemented in nfcpy. + + """ + if hasattr(self, "_format"): + args = "version={0!r}, wipe={1!r}" + args = args.format(version, wipe) + log.debug("format({0})".format(args)) + status = self._format(version, wipe) + if status is True: + self._ndef = None + return status + else: + log.debug("this tag can not be formatted with nfcpy") + return None + + def protect(self, password=None, read_protect=False, protect_from=0): + """Protect a tag against future write or read access. + + :meth:`protect` attempts to make a tag readonly for all + readers if *password* is :const:`None`, writeable only after + authentication if a *password* is provided, and readable only + after authentication if a *password* is provided and the + *read_protect* flag is set. The *password* must be a byte or + character sequence that provides sufficient key material for + the tag specific protect function (this is documented + separately for the individual tag types). As a special case, + if *password* is set to an empty string the :meth:`protect` + method uses a default manufacturer value if such is known. + + The *protect_from* argument sets the first memory unit to be + protected. Memory units are tag type specific, for a Type 1 or + Type 2 Tag a memory unit is 4 byte, for a Type 3 Tag it is 16 + byte, and for a Type 4 Tag it is the complete NDEF data area. + + Note that the effect of protecting a tag without password can + normally not be reversed. + + The return value of :meth:`protect` is either :const:`True` or + :const:`False` depending on whether the operation was + successful or not, or :const:`None` if the tag does not + support custom protection (or it is not implemented). + + """ + if hasattr(self, "_protect"): + args = "password={0!r}, read_protect={1!r}, protect_from={2!r}" + args = args.format(password, read_protect, protect_from) + log.debug("protect({0})".format(args)) + status = self._protect(password, read_protect, protect_from) + if status is True: + self._ndef = None + return status + else: + log.error("this tag can not be protected with nfcpy") + return None + + def authenticate(self, password): + """Authenticate a tag with a *password*. + + A tag that was once protected with a password requires + authentication before write, potentially also read, operations + may be performed. The *password* must be the same as the + password provided to :meth:`protect`. The return value + indicates authentication success with :const:`True` or + :const:`False`. For a tag that does not support authentication + the return value is :const:`None`. + + """ + if hasattr(self, "_authenticate"): + args = "password={0!r}".format(password) + log.debug("authenticate({0})".format(args)) + self._authenticated = self._authenticate(password) + if self._authenticated is True: + self._ndef = None + return self._authenticated + else: + log.error("this tag can not be authenticated with nfcpy") + return None + + +TIMEOUT_ERROR = 0 +RECEIVE_ERROR = -1 +PROTOCOL_ERROR = -2 + + +class TagCommandError(Exception): + """The base class for exceptions that are raised when a tag command + has not returned the expected result or a a lower stack error was + raised. + + The :attr:`errno` attribute holds a reason code for why the + command has failed. Error numbers greater than zero indicate a tag + type specific error from one of the exception classes derived from + :exc:`TagCommandError` (per tag type module). Error numbers below + and including zero indicate general errors:: + + nfc.tag.TIMEOUT_ERROR => unrecoverable timeout error + nfc.tag.RECEIVE_ERROR => unrecoverable transmission error + nfc.tag.PROTOCOL_ERROR => unrecoverable protocol error + + The :exc:`TagCommandError` exception populates the *message* + attribute of the general exception class with the appropriate + error description. + + """ + errno_str = { + TIMEOUT_ERROR: "unrecoverable timeout error", + RECEIVE_ERROR: "unrecoverable transmission error", + PROTOCOL_ERROR: "unrecoverable protocol error", + } + + def __init__(self, errno): + default = "tag command error {errno} (0x{errno:x})".format(errno=errno) + if errno > 0: + message = self.errno_str.get(errno, default) + else: + message = TagCommandError.errno_str.get(errno, default) + super(TagCommandError, self).__init__(message) + self._errno = errno + + @property + def errno(self): + """Holds the error reason code.""" + return self._errno + + def __int__(self): + return self._errno + + +def activate(clf, target): + import nfc.clf + try: + log.debug("trying to activate {0}".format(target)) + if target.brty.endswith('A'): + if target.sens_res[1] & 0x0F == 0x0C: + return activate_tt1(clf, target) + elif target.sel_res[0] >> 5 & 3 == 0: + return activate_tt2(clf, target) + elif target.sel_res[0] >> 5 & 1 == 1: + return activate_tt4(clf, target) + elif target.brty.endswith('B'): + return activate_tt4(clf, target) + elif target.brty.endswith('F'): + return activate_tt3(clf, target) + except nfc.clf.CommunicationError: + return None + + +def activate_tt1(clf, target): + log.debug("trying type 1 tag activation for {0}".format(target.brty)) + import nfc.tag.tt1 + return nfc.tag.tt1.activate(clf, target) + + +def activate_tt2(clf, target): + log.debug("trying type 2 tag activation for {0}".format(target.brty)) + import nfc.tag.tt2 + return nfc.tag.tt2.activate(clf, target) + + +def activate_tt3(clf, target): + log.debug("trying type 3 tag activation for {0}".format(target.brty)) + import nfc.tag.tt3 + return nfc.tag.tt3.activate(clf, target) + + +def activate_tt4(clf, target): + log.debug("trying type 4 tag activation for {0}".format(target.brty)) + import nfc.tag.tt4 + return nfc.tag.tt4.activate(clf, target) + + +class TagEmulation(object): + """Base class for tag emulation classes.""" + pass + + +def emulate(clf, target): + import nfc.clf + assert isinstance(target, nfc.clf.LocalTarget) + if target.tt3_cmd: + import nfc.tag.tt3 + return nfc.tag.tt3.Type3TagEmulation(clf, target) + else: + log.debug("can't emulate with %s", target) diff --git a/src/lib/nfc/tag/tt1.py b/src/lib/nfc/tag/tt1.py new file mode 100644 index 0000000..3e77996 --- /dev/null +++ b/src/lib/nfc/tag/tt1.py @@ -0,0 +1,555 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2011, 2017 +# Stephen Tiedemann +# Alexander Knaub +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import time +from binascii import hexlify +from struct import pack, unpack + +from . import Tag, TagCommandError +import nfc.clf + +import logging +log = logging.getLogger(__name__) + + +CHECKSUM_ERROR, RESPONSE_ERROR, WRITE_ERROR, \ + BLOCK_ERROR, SECTOR_ERROR = range(1, 6) + + +class Type1TagCommandError(TagCommandError): + """Type 1 Tag specific exceptions. Sets + :attr:`~nfc.tag.TagCommandError.errno` to one of: + + | 1 - CHECKSUM_ERROR + | 2 - RESPONSE_ERROR + | 3 - WRITE_ERROR + + """ + errno_str = { + CHECKSUM_ERROR: "crc validation failed", + RESPONSE_ERROR: "invalid response data", + WRITE_ERROR: "data write failure", + } + + +def read_tlv(memory, offset, skip_bytes): + # Unpack a TLV from tag memory and return tag type, tag length and + # tag value. For tag type 0 there is no length field, this is + # returned as length -1. The tlv length field can be one or three + # bytes, if the first byte is 255 then the next two byte carry the + # length (big endian). + try: + tlv_t, offset = (memory[offset], offset+1) + except Type1TagCommandError: + return (None, None, None) + + if tlv_t in (0x00, 0xFE): + return (tlv_t, -1, None) + + tlv_l, offset = (memory[offset], offset+1) + + if tlv_l == 0xFF: + tlv_l, offset = (unpack(">H", memory[offset:offset+2])[0], offset+2) + + tlv_v = bytearray(tlv_l) + for i in range(tlv_l): + while (offset + i) in skip_bytes: + offset += 1 + tlv_v[i] = memory[offset+i] + + return (tlv_t, tlv_l, tlv_v) + + +def get_lock_byte_range(data): + # Extract the lock byte range indicated by a Lock Control TLV. The + # data argument is the TLV value field. + page_addr = data[0] >> 4 + byte_offs = data[0] & 0x0F + rsvd_size = ((data[1] if data[1] > 0 else 256) + 7) // 8 + page_size = 2 ** (data[2] & 0x0F) + rsvd_from = page_addr * page_size + byte_offs + return slice(rsvd_from, rsvd_from + rsvd_size) + + +def get_rsvd_byte_range(data): + # Extract the reserved memory range indicated by a Memory Control + # TLV. The data argument is the TLV value field. + page_addr = data[0] >> 4 + byte_offs = data[0] & 0x0F + rsvd_size = data[1] if data[1] > 0 else 256 + page_size = 2 ** (data[2] & 0x0F) + rsvd_from = page_addr * page_size + byte_offs + return slice(rsvd_from, rsvd_from + rsvd_size) + + +def get_capacity(tag_memory_size, offset, skip_bytes): + # The net capacity is the range of bytes from the current offset + # until the end of user data bytes (given by the capability + # container capacity value plus 16 header bytes), reduced by the + # number of skip bytes (from memory and lock control TLVs) that + # are within the usable memory range, and adjusted by the required + # number of TLV length bytes (1 or 3) and the TLV tag byte. + log.debug("subtract {0} skip bytes from capacity".format(len(skip_bytes))) + capacity = len(set(range(offset, tag_memory_size)) - skip_bytes) + # To store more than 254 byte ndef we must use three length bytes, + # otherwise it's only one. But only if the capacity is more than + # 256 the three length byte format will provide a higher value. + capacity -= 4 if capacity > 256 else 2 + return capacity + + +class Type1Tag(Tag): + """Implementation of the NFC Forum Type 1 Tag Operation specification. + + The NFC Forum Type 1 Tag is based on the ISO 14443 Type A + technology for frame structure and anticollision (detection) + commands, and the Innovision (now Broadcom) Jewel/Topaz commands + for accessing the tag memory. + + """ + TYPE = "Type1Tag" + + class NDEF(Tag.NDEF): + # Type 1 Tag specific implementation of the NDEF access type + # class that is returned by the Tag.ndef attribute. + + def __init__(self, tag): + super(Type1Tag.NDEF, self).__init__(tag) + self._ndef_tlv_offset = 0 + + def _read_ndef_data(self): + # Check and read ndef data from tag. Return None if the + # tag is not ndef formatted, i.e. it can not hold ndef + # data or does not have (valid) ndef management data. + # Otherwise, set state variables and return the ndef + # message data as a bytearray (may be zero length). + log.debug("read ndef data") + try: + tag_memory = Type1TagMemoryReader(self.tag) + + if tag_memory._header_rom[0] >> 4 != 1: + log.debug("proprietary type 1 tag memory structure") + return None + + if tag_memory[8] != 0xE1: + log.debug("ndef management data is not present") + return None + + if tag_memory[9] >> 4 != 1: + log.debug("unsupported ndef mapping version") + return None + + self._readable = bool(tag_memory[11] >> 4 == 0) + self._writeable = bool(tag_memory[11] & 0xF == 0) + + tag_memory_size = (tag_memory[10] + 1) * 8 + log.debug("tag memory size is %d byte" % tag_memory_size) + except Type1TagCommandError: + log.debug("header rom and static memory were unreadable") + return None + + ndef = None + offset = 12 + skip_end = 120 if tag_memory_size == 120 else 128 + skip_bytes = set(range(104, skip_end)) + while offset < tag_memory_size: + if offset in skip_bytes: + offset += 1 + continue + + tlv_t, tlv_l, tlv_v = read_tlv(tag_memory, offset, skip_bytes) + log.debug("tlv type {0} at address {1}".format(tlv_t, offset)) + + if tlv_t == 0x00: + pass + elif tlv_t == 0x01: + lock_bytes = get_lock_byte_range(tlv_v) + skip_bytes.update(range(*lock_bytes.indices(0x800))) + elif tlv_t == 0x02: + rsvd_bytes = get_rsvd_byte_range(tlv_v) + skip_bytes.update(range(*rsvd_bytes.indices(0x800))) + elif tlv_t == 0x03: + ndef = tlv_v + break + elif tlv_t == 0xFE or tlv_t is None: + break + else: + logmsg = "unknown tlv {0} at offset {0}" + log.debug(logmsg.format(tlv_t, offset)) + + offset += tlv_l + 1 + (1 if tlv_l < 255 else 3) + + self._capacity = get_capacity(tag_memory_size, offset, skip_bytes) + self._ndef_tlv_offset = offset + self._tag_memory = tag_memory + self._skip_bytes = skip_bytes + return ndef + + def _write_ndef_data(self, data): + log.debug("write ndef data {0}{1}".format( + hexlify(data[:10]).decode(), '...' if len(data) > 10 else '')) + + tag_memory = self._tag_memory + skip_bytes = self._skip_bytes + offset = self._ndef_tlv_offset + tag_memory_size = (tag_memory[10] + 1) * 8 + + # Set the ndef message tlv length to 0. + tag_memory[offset+1] = 0 + tag_memory.synchronize() + + # Leave room for ndef message length byte(s) and write + # ndef data into the memory image, but jump over skip + # bytes. + offset += 2 if len(data) < 255 else 4 + for i in range(len(data)): + while offset + i in skip_bytes: + offset += 1 + tag_memory[offset+i] = data[i] + # Write a terminator tlv if space permits. We may have to + # skip reserved and lock bytes. + offset = offset + i + 1 + while offset < tag_memory_size: + if offset not in skip_bytes: + tag_memory[offset] = 0xFE + break + offset += 1 + # Write the new message data to the tag. + tag_memory.synchronize() + + # Write the ndef message tlv length. + offset = self._ndef_tlv_offset + if len(data) < 255: + tag_memory[offset+1] = len(data) + else: + tag_memory[offset+1] = 0xFF + tag_memory[offset+2:offset+4] = pack(">H", len(data)) + tag_memory.synchronize() + + # + # Type1Tag methods and attributes + # + def __init__(self, clf, target): + super(Type1Tag, self).__init__(clf, target) + self._nfcid = self.uid = target.rid_res[2:6] + + def dump(self): + """Returns the tag memory blocks as a list of formatted strings. + + :meth:`dump` iterates over all tag memory blocks (8 bytes + each) from block zero until the physical end of memory and + produces a list of strings that is intended for line by line + printing. Multiple consecutive memory block of identical + content may be reduced to fewer lines of output, so the number + of lines returned does not necessarily correspond to the + number of memory blocks present. + + .. warning:: For tags with more than 120 byte memory, the + dump() method first overwrites the data block to verify + that it is backed by physical memory, then restores the + original data. This is necessary because Type 1 Tags do + not indicate an error when reading beyond the physical + memory space. Be cautious to not remove a tag from the + reader when using dump() as otherwise your data may be + corrupted. + + """ + return self._dump(stop=None) + + def _dump(self, stop=None): + # Read and print all data blocks until the non-inclusive stop + # block number. Type 1 Tags with dynamic memory seem to return + # data for every address, regardless of whether there is + # memory mapped or not. To show exactly the memory blocks that + # are physically present, blocks from 16-end are first + # overwritten with an inverted version of the content and then + # recovered. Because WRITE8 returns the new data content, a + # non-existing block can be detected. + + def oprint(octets): + return ' '.join(['??' if x < 0 else '%02x' % x for x in octets]) + + def cprint(octets): + return ''.join([chr(x) if 32 <= x <= 126 else '.' for x in octets]) + + def lprint(fmt, d, i): + return fmt.format(i, oprint(d), cprint(d)) + + txt = ["UID0-UID6, RESERVED", "RESERVED", "LOCK0-LOCK1, OTP0-OTP5", + "LOCK2-LOCK3, RESERVED"] + + lines = list() + data = self.read_all() + hrom, data = data[0:2], data[2:] + + lines.append("HR0={0:02X}h, HR1={1:02X}h".format(*hrom)) + lines.append(" 0: {0} ({1})".format(oprint(data[0:8]), txt[0])) + for i in range(8, 104, 8): + lines.append(lprint("{0:3}: {1} |{2}|", data[i:i+8], i//8)) + lines.append(" 13: {0} ({1})".format(oprint(data[104:112]), txt[1])) + lines.append(" 14: {0} ({1})".format(oprint(data[112:120]), txt[2])) + + if stop is None or stop > 15: + try: + data = self.read_block(15) + except Type1TagCommandError: + return lines + else: + lines.append(" 15: {0} ({1})".format(oprint(data), txt[3])) + + data_line_fmt = "{0:>3}: {1} |{2}|" + same_line_fmt = "{0:>3} {1} |{2}|" + this_data = last_data = None + same_data = 0 + + def dump_same_data(same_data, last_data, this_data, page): + if same_data > 1: + lines.append(lprint(same_line_fmt, last_data, "*")) + if same_data > 0: + lines.append(lprint(data_line_fmt, this_data, page)) + + for i in range(16, stop if stop is not None else 256): + try: + this_data = self.read_block(i) + if stop is None: + test_data = bytearray([b ^ 0xFF for b in this_data]) + self.write_block(i, test_data) + self.write_block(i, this_data) + except Type1TagCommandError: + dump_same_data(same_data, last_data, this_data, i-1) + break + + if this_data == last_data: + same_data += 1 + else: + dump_same_data(same_data, last_data, last_data, i-1) + lines.append(lprint(data_line_fmt, this_data, i)) + last_data = this_data + same_data = 0 + else: + dump_same_data(same_data, last_data, this_data, i) + + return lines + + def protect(self, password=None, read_protect=False, protect_from=0): + """The implementation of :meth:`nfc.tag.Tag.protect` for a generic + type 1 tag is limited to setting the NDEF data read-only for + tags that are already NDEF formatted. + + """ + return super(Type1Tag, self).protect( + password, read_protect, protect_from) + + def _protect(self, password, read_protect, protect_from): + if password is None: + if self.ndef is not None: + self.write_byte(11, 0x0F, erase=False) + return True + else: + log.warning("no ndef, can't set write access restriction") + else: + log.warning("this tag can not be protected with a password") + return False + + def _is_present(self): + try: + return self.read_byte(0) == self.uid[0] + except Type1TagCommandError: + return False + + def read_id(self): + """Returns the 2 byte Header ROM and 4 byte UID. + """ + log.debug("read identification") + cmd = b"\x78\x00\x00\x00\x00\x00\x00" + return self.transceive(cmd) + + def read_all(self): + """Returns the 2 byte Header ROM and all 120 byte static memory. + """ + log.debug("read all static memory") + cmd = b"\x00\x00\x00" + self.uid + return self.transceive(cmd) + + def read_byte(self, addr): + """Read a single byte from static memory area (blocks 0-14). + """ + if addr < 0 or addr > 127: + raise ValueError("invalid byte address") + log.debug("read byte at address {0} ({0:02X}h)".format(addr)) + cmd = bytearray([0x01, addr, 0x00]) + self.uid + return self.transceive(cmd)[-1] + + def read_block(self, block): + """Read an 8-byte data block at address (block * 8). + """ + if block < 0 or block > 255: + raise ValueError("invalid block number") + log.debug("read block {0}".format(block)) + cmd = bytearray([0x02, block] + [0x00 for _ in range(8)]) + self.uid + return self.transceive(cmd)[1:9] + + def read_segment(self, segment): + """Read one memory segment (128 byte). + """ + log.debug("read segment {0}".format(segment)) + if segment < 0 or segment > 15: + raise ValueError("invalid segment number") + cmd = bytearray([0x10, segment << 4] + [0x00 for _ in range(8)]) \ + + self.uid + rsp = self.transceive(cmd) + if len(rsp) < 129: + raise Type1TagCommandError(RESPONSE_ERROR) + return rsp[1:129] + + def write_byte(self, addr, data, erase=True): + """Write a single byte to static memory area (blocks 0-14). The + target byte is zero'd first if *erase* is True. + + """ + if addr < 0 or addr >= 128: + raise ValueError("invalid byte address") + log.debug("write byte at address {0} ({0:02X}h)".format(addr)) + cmd = b"\x53" if erase is True else b"\x1A" + cmd = cmd + bytearray([addr, data]) + self.uid + return self.transceive(cmd) + + def write_block(self, block, data, erase=True): + """Write an 8-byte data block at address (block * 8). The target + bytes are zero'd first if *erase* is True. + + """ + if block < 0 or block > 255: + raise ValueError("invalid block number") + log.debug("write block {0}".format(block)) + cmd = b"\x54" if erase is True else b"\x1B" + cmd = cmd + bytearray([block]) + data + self.uid + rsp = self.transceive(cmd) + if len(rsp) < 9: + raise Type1TagCommandError(RESPONSE_ERROR) + if erase is True and rsp[1:9] != data: + raise Type1TagCommandError(WRITE_ERROR) + + def transceive(self, data, timeout=0.1): + log.debug(">> {0} ({1:f}s)".format(hexlify(data).decode(), timeout)) + + started = time.time() + error = None + for retry in range(3): + try: + data = self.clf.exchange(data, timeout) + break + except nfc.clf.CommunicationError as e: + error = e + reason = error.__class__.__name__ + log.debug("%s after %d retries" % (reason, retry)) + else: + if type(error) is nfc.clf.TimeoutError: + raise Type1TagCommandError(nfc.tag.TIMEOUT_ERROR) + if type(error) is nfc.clf.TransmissionError: + raise Type1TagCommandError(nfc.tag.RECEIVE_ERROR) + if type(error) is nfc.clf.ProtocolError: + raise Type1TagCommandError(nfc.tag.PROTOCOL_ERROR) + raise RuntimeError("unexpected " + repr(error)) + + elapsed = time.time() - started + log.debug("<< {0} ({1:f}s)".format(hexlify(data).decode(), elapsed)) + return data + + +class Type1TagMemoryReader(object): + def __init__(self, tag): + assert isinstance(tag, Type1Tag) + self._data_from_tag = bytearray() + self._data_in_cache = bytearray() + self._tag = tag + self._header_rom = bytearray(0) + # read header_rom and static memory + self._read_from_tag(1) + + def __len__(self): + return len(self._data_from_tag) + + def __getitem__(self, key): + if isinstance(key, slice): + start, stop, step = key.indices(0x100000) + if stop > len(self): + self._read_from_tag(stop) + elif key >= len(self): + self._read_from_tag(stop=key+1) + return self._data_in_cache[key] + + def __setitem__(self, key, value): + self.__getitem__(key) + if isinstance(key, slice): + if len(value) != len(range(*key.indices(0x100000))): + msg = "{cls} requires item assignment of identical length" + raise ValueError(msg.format(cls=self.__class__.__name__)) + self._data_in_cache[key] = value + del self._data_in_cache[len(self):] + + def __delitem__(self, key): + msg = "{cls} object does not support item deletion" + raise TypeError(msg.format(cls=self.__class__.__name__)) + + def _read_from_tag(self, stop): + if len(self) < 120: + read_all_data_response = self._tag.read_all() + self._header_rom = read_all_data_response[0:2] + self._data_from_tag[0:] = read_all_data_response[2:] + self._data_in_cache[0:] = self._data_from_tag[0:] + + if stop > 120 and len(self) < 128: + read_block_response = self._tag.read_block(15) + self._data_from_tag[120:128] = read_block_response + self._data_in_cache[120:128] = read_block_response + + while len(self) < stop: + data = self._tag.read_segment(len(self) >> 7) + self._data_from_tag.extend(data) + self._data_in_cache.extend(data) + + def _write_to_tag(self, stop): + hr0 = self._header_rom[0] + if hr0 >> 4 == 1 and hr0 & 0x0F != 1: + for i in range(0, stop, 8): + data = self._data_in_cache[i:i+8] + if data != self._data_from_tag[i:i+8]: + self._tag.write_block(i//8, data) + self._data_from_tag[i:i+8] = data + else: + for i in range(0, stop): + data = self._data_in_cache[i] + if data != self._data_from_tag[i]: + self._tag.write_byte(i, data) + self._data_from_tag[i] = data + + def synchronize(self): + """Write pages that contain modified data back to tag memory.""" + self._write_to_tag(stop=len(self)) + + +def activate(clf, target): + import nfc.tag.tt1_broadcom + tag = nfc.tag.tt1_broadcom.activate(clf, target) + return tag if tag is not None else Type1Tag(clf, target) diff --git a/src/lib/nfc/tag/tt1_broadcom.py b/src/lib/nfc/tag/tt1_broadcom.py new file mode 100644 index 0000000..7f700fe --- /dev/null +++ b/src/lib/nfc/tag/tt1_broadcom.py @@ -0,0 +1,159 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2014, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +from . import tt1 + +import logging +log = logging.getLogger(__name__) + + +class Topaz(tt1.Type1Tag): + """The Broadcom Topaz is a small memory tag that can hold up to 94 + byte ndef message data. + + """ + def __init__(self, clf, target): + super(Topaz, self).__init__(clf, target) + self._product = "Topaz (BCM20203T96)" + + def dump(self): + return super(Topaz, self)._dump(stop=15) + + def format(self, version=None, wipe=None): + """Format a Topaz tag for NDEF use. + + The implementation of :meth:`nfc.tag.Tag.format` for a Topaz + tag creates a capability container and an NDEF TLV with length + zero. Data bytes of the NDEF data area are left untouched + unless the wipe argument is set. + + """ + return super(Topaz, self).format(version, wipe) + + def _format(self, version, wipe): + tag_memory = tt1.Type1TagMemoryReader(self) + tag_memory[8:14] = b"\xE1\x10\x0E\x00\x03\x00" + + if version is not None: + if version >> 4 == 1: + tag_memory[9] = version + else: + log.warning("can not format with major version != 1") + return False + + if wipe is not None: + tag_memory[14:104] = bytearray([wipe & 0xFF]) * 90 + + tag_memory.synchronize() + return True + + def protect(self, password=None, read_protect=False, protect_from=0): + """In addtion to :meth:`nfc.tag.tt1.Type1Tag.protect` this method + tries to set the lock bits to irreversibly protect the tag + memory. However, it appears that tags sold have the lock bytes + write protected, so this additional effort most likely doesn't + have any effect. + + """ + return super(Topaz, self).protect( + password, read_protect, protect_from) + + def _protect(self, password, read_protect, protect_from): + if super(Topaz, self)._protect(password, read_protect, protect_from): + self.write_byte(112, 0xFF, erase=False) + self.write_byte(113, 0xFF, erase=False) + return True + else: + return False + + +class Topaz512(tt1.Type1Tag): + """The Broadcom Topaz-512 is a memory enhanced version that can hold + up to 462 byte ndef message data. + + """ + def __init__(self, clf, target): + super(Topaz512, self).__init__(clf, target) + self._product = "Topaz 512 (BCM20203T512)" + + def dump(self): + return super(Topaz512, self)._dump(stop=64) + + def format(self, version=None, wipe=None): + """Format a Topaz-512 tag for NDEF use. + + The implementation of :meth:`nfc.tag.Tag.format` for a + Topaz-512 tag creates a capability container, a Lock Control + and a Memory Control TLV, and an NDEF TLV with length + zero. Data bytes of the NDEF data area are left untouched + unless the wipe argument is set. + + """ + return super(Topaz512, self).format(version, wipe) + + def _format(self, version, wipe): + tag_memory = tt1.Type1TagMemoryReader(self) + tag_memory[8:16] = bytearray.fromhex("E1103F000103F230") + tag_memory[16:24] = bytearray.fromhex("330203F002030300") + + if version is not None: + if version >> 4 == 1: + tag_memory[9] = version + else: + log.warning("can not format with major version != 1") + return False + + if wipe is not None: + tag_memory[24:104] = bytearray([wipe & 0xFF]) * 80 + tag_memory[128:512] = bytearray([wipe & 0xFF]) * 384 + + tag_memory.synchronize() + return True + + def protect(self, password=None, read_protect=False, protect_from=0): + """In addtion to :meth:`nfc.tag.tt1.Type1Tag.protect` this method + tries to set the lock bits to irreversibly protect the tag + memory. However, it appears that tags sold have the lock bytes + write protected, so this additional effort most likely doesn't + have any effect. + + """ + return super(Topaz512, self).protect( + password, read_protect, protect_from) + + def _protect(self, password, read_protect, protect_from): + if super(Topaz512, self)._protect( + password, read_protect, protect_from): + self.write_byte(112, 0xFF, erase=False) + self.write_byte(113, 0xFF, erase=False) + self.write_byte(120, 0xFF, erase=False) + self.write_byte(121, 0xFF, erase=False) + return True + else: + return False + + +def activate(clf, target): + hrom = target.rid_res[0:2] + if hrom == b"\x11\x48": + return Topaz(clf, target) + if hrom == b"\x12\x4C": + return Topaz512(clf, target) diff --git a/src/lib/nfc/tag/tt2.py b/src/lib/nfc/tag/tt2.py new file mode 100644 index 0000000..f26120c --- /dev/null +++ b/src/lib/nfc/tag/tt2.py @@ -0,0 +1,697 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# Licensed under the EUPL, Version 1.1 or - as soon they +# You may obtain a copy of the Licence at: +# + +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import time +from binascii import hexlify +from struct import pack, unpack + +from . import Tag, TagCommandError +import nfc.clf + +import logging +log = logging.getLogger(__name__) + + +def hexdump(octets, sep=""): + return sep.join( + ("??" if x is None else ("%02x" % x)) for x in octets) + + +def chrdump(octets, sep=""): + return sep.join( + (("{:c}".format(x) if 32 <= x <= 126 else ".") + if x is not None + else ".") + for x in octets) + + +def pagedump(page, octets, info=None): + info = ("|%s|" % chrdump(octets)) if info is None else ("(%s)" % info) + page = " * " if page is None else "{0:03X}:".format(page) + return "{0} {1} {2}".format(page, hexdump(octets, sep=" "), info) + + +TIMEOUT_ERROR, INVALID_SECTOR_ERROR, \ + INVALID_PAGE_ERROR, INVALID_RESPONSE_ERROR = range(4) + + +class Type2TagCommandError(TagCommandError): + """Type 2 Tag specific exceptions. Sets + :attr:`~nfc.tag.TagCommandError.errno` to one of: + + | 1 - INVALID_SECTOR_ERROR + | 2 - INVALID_PAGE_ERROR + | 3 - INVALID_RESPONSE_ERROR + + """ + errno_str = { + INVALID_SECTOR_ERROR: "invalid sector number", + INVALID_PAGE_ERROR: "invalid page number", + INVALID_RESPONSE_ERROR: "invalid response data", + } + + +def read_tlv(memory, offset, skip_bytes): + # Unpack a Type 2 Tag TLV from tag memory and return tag type, tag + # length and tag value. For tag type 0 there is no length field, + # this is returned as length -1. The tlv length field can be one + # or three bytes, if the first byte is 255 then the next two byte + # carry the length (big endian). + tlv_t, offset = (memory[offset], offset+1) + if tlv_t in (0x00, 0xFE): + return (tlv_t, -1, None) + tlv_l, offset = (memory[offset], offset+1) + if tlv_l == 0xFF: + tlv_l, offset = (unpack(">H", memory[offset:offset+2])[0], offset+2) + tlv_v = bytearray(tlv_l) + for i in range(tlv_l): + while (offset + i) in skip_bytes: + offset += 1 + tlv_v[i] = memory[offset+i] + return (tlv_t, tlv_l, tlv_v) + + +def get_lock_byte_range(data): + # Extract the lock byte range indicated by a Lock Control TLV. The + # data argument is the TLV value field. + page_addr = data[0] >> 4 + byte_offs = data[0] & 0x0F + rsvd_size = ((data[1] if data[1] > 0 else 256) + 7) // 8 + page_size = 2 ** (data[2] & 0x0F) + rsvd_from = page_addr * page_size + byte_offs + return slice(rsvd_from, rsvd_from + rsvd_size) + + +def get_rsvd_byte_range(data): + # Extract the reserved memory range indicated by a Memory Control + # TLV. The data argument is the TLV value field. + page_addr = data[0] >> 4 + byte_offs = data[0] & 0x0F + rsvd_size = data[1] if data[1] > 0 else 256 + page_size = 2 ** (data[2] & 0x0F) + rsvd_from = page_addr * page_size + byte_offs + return slice(rsvd_from, rsvd_from + rsvd_size) + + +def get_capacity(capacity, offset, skip_bytes): + # The net capacity is the range of bytes from the current offset + # until the end of user data bytes (given by the capability + # container capacity value plus 16 header bytes), reduced by the + # number of skip bytes (from memory and lock control TLVs) that + # are within the usable memory range, and adjusted by the required + # number of TLV length bytes (1 or 3) and the TLV tag byte. + capacity = len(set(range(offset, capacity + 16)) - skip_bytes) + # To store more than 254 byte ndef we must use three length bytes, + # otherwise it's only one. But only if the capacity is more than + # 256 the three length byte format will provide a higher value. + capacity -= 4 if capacity > 256 else 2 + return capacity + + +class Type2Tag(Tag): + """Implementation of the NFC Forum Type 2 Tag Operation specification. + + The NFC Forum Type 2 Tag is based on the ISO 14443 Type A + technology for frame structure and anticollision (detection) + commands, and the NXP Mifare commands for accessing the tag + memory. + + """ + TYPE = "Type2Tag" + + class NDEF(Tag.NDEF): + # Type 2 Tag specific implementation of the NDEF access type + # class that is returned by the Tag.ndef attribute. + + def __init__(self, tag): + super(Type2Tag.NDEF, self).__init__(tag) + self._ndef_tlv_offset = 0 + + def _read_capability_data(self, tag_memory): + try: + if tag_memory[12] != 0xE1: + log.debug("ndef management data is not present") + return False + if tag_memory[13] >> 4 != 1: + log.debug("unsupported ndef mapping major version") + return False + self._readable = bool(tag_memory[15] >> 4 == 0) + self._writeable = bool(tag_memory[15] & 0xF == 0) + return True + except Type2TagCommandError: + log.debug("first four memory pages were unreadable") + return False + + def _read_ndef_data(self): + log.debug("read ndef data") + tag_memory = Type2TagMemoryReader(self.tag) + + if not self._read_capability_data(tag_memory): + return None + + raw_capacity = tag_memory[14] * 8 + log.debug("raw capacity is {0} byte".format(raw_capacity)) + + offset = 16 + ndef = None + skip_bytes = set() + data_area_size = raw_capacity + while offset < data_area_size + 16: + while (offset) in skip_bytes: + offset += 1 + + try: + tlv = read_tlv(tag_memory, offset, skip_bytes) + tlv_t, tlv_l, tlv_v = tlv + except Type2TagCommandError: + return None + else: + logmsg = "tlv type {0} length {1} at offset {2}" + log.debug(logmsg.format(tlv_t, tlv_l, offset)) + + if tlv_t == 0: + pass + elif tlv_t == 1: + if tlv_l == 3: + lock_bytes = get_lock_byte_range(tlv_v) + skip_bytes.update(range(*lock_bytes.indices(0x100000))) + else: + log.debug("lock tlv has wrong length") + elif tlv_t == 2: + if tlv_l == 3: + rsvd_bytes = get_rsvd_byte_range(tlv_v) + skip_bytes.update(range(*rsvd_bytes.indices(0x100000))) + else: + log.debug("memory tlv has wrong length") + elif tlv_t == 3: + ndef = tlv_v + break + elif tlv_t == 254: + break + else: + logmsg = "unknown tlv {0} at offset {0}" + log.debug(logmsg.format(tlv_t, offset)) + + offset += tlv_l + 1 + (1 if tlv_l < 255 else 3) + + self._capacity = get_capacity(raw_capacity, offset, skip_bytes) + self._ndef_tlv_offset = offset + self._tag_memory = tag_memory + self._skip_bytes = skip_bytes + return ndef + + def _write_ndef_data(self, data): + # Write new ndef data to the tag memory. Despite the + # tag memory is rather easy to handle, the extremely + # generic NFC Forum TLV structure makes this rather + # complicated. The precondition is that we have already + # processed the memory structure in _read_ndef_data(), if + # not we'll do it first. We'll then have a tag memory + # image, know which bytes need to be to skipped as told by + # memory or control tlv data, and where the ndef message + # tlv starts. We first set the ndef message tlv length to + # zero (synchronize cause that to be actually written), + # then write all new data into the memory image (skipping + # bytes as needed) and let that be written to the tag, and + # finally write the new ndef message tlv length. + log.debug("write ndef data {0}{1}".format( + hexlify(data[:10]).decode(), '...' if len(data) > 10 else '')) + + tag_memory = self._tag_memory + skip_bytes = self._skip_bytes + offset = self._ndef_tlv_offset + + # Set the ndef message tlv length to 0. + tag_memory[offset+1] = 0 + tag_memory.synchronize() + + # Leave room for ndef message length byte(s) and write + # ndef data into the memory image, but jump over skip + # bytes. If space permits, write a terminator tlv. + offset += 2 if len(data) < 255 else 4 + for index, octet in enumerate(data): + while offset + index in skip_bytes: + offset += 1 + tag_memory[offset+index] = octet + offset = offset + index + 1 + while offset in skip_bytes: + offset += 1 + if offset < tag_memory[14] * 8 + 16: + tag_memory[offset] = 0xFE + tag_memory.synchronize() + + # Write the ndef message tlv length. + offset = self._ndef_tlv_offset + if len(data) < 255: + tag_memory[offset+1] = len(data) + else: + tag_memory[offset+1] = 0xFF + tag_memory[offset+2:offset+4] = pack(">H", len(data)) + tag_memory.synchronize() + + # + # Type2Tag methods and attributes + # + def __init__(self, clf, target): + super(Type2Tag, self).__init__(clf, target) + self._nfcid = bytearray(target.sdd_res) + self._current_sector = 0 + + def dump(self): + """Returns the tag memory pages as a list of formatted strings. + + :meth:`dump` iterates over all tag memory pages (4 bytes + each) from page zero until an error response is received and + produces a list of strings that is intended for line by line + printing. Note that multiple consecutive memory pages of + identical content may be reduced to fewer lines of output, so + the number of lines returned does not necessarily correspond + to the number of memory pages. + + """ + return self._dump(stop=None) + + def _dump(self, stop=None): + lines = list() + header = ("UID0-UID2, BCC0", "UID3-UID6", + "BCC1, INT, LOCK0-LOCK1", "OTP0-OTP3") + + for i, info in enumerate(header): + try: + data = self.read(i)[0:4] + except Type2TagCommandError: + data = [None, None, None, None] + lines.append(pagedump(i, data, info)) + + this_data = last_data = None + same_data = 0 + + def dump_same_data(same_data, last_data, this_data, page): + if same_data > 1: + lines.append(pagedump(None, this_data)) + if same_data > 0: + lines.append(pagedump(page, this_data)) + + for i in range(4, stop if stop is not None else 0x40000): + try: + self.sector_select(i >> 8) + this_data = self.read(i)[0:4] + except Type2TagCommandError: + dump_same_data(same_data, last_data, this_data, i-1) + if stop is not None: + this_data = last_data = [None, None, None, None] + lines.append(pagedump(i, this_data)) + dump_same_data(stop-i-1, this_data, this_data, stop-1) + break + + if this_data == last_data: + same_data += 1 + else: + dump_same_data(same_data, last_data, last_data, i-1) + lines.append(pagedump(i, this_data)) + last_data = this_data + same_data = 0 + else: + dump_same_data(same_data, last_data, this_data, i) + + return lines + + def _is_present(self): + # Verify that the tag is still present. This is implemented as + # reading page 0-3 (from whatever sector is currently active). + try: + data = self.transceive(b"\x30\x00") + except Type2TagCommandError as error: + if error.errno != TIMEOUT_ERROR: + log.warning("unexpected error in presence check: %s" % error) + return False + else: + return bool(data and len(data) == 16) + + def format(self, version=None, wipe=None): + """Erase the NDEF message on a Type 2 Tag. + + The :meth:`format` method will reset the length of the NDEF + message on a type 2 tag to zero, thus the tag will appear to + be empty. Additionally, if the *wipe* argument is set to some + integer then :meth:`format` will overwrite all user date that + follows the NDEF message TLV with that integer (mod 256). If + an NDEF message TLV is not present it will be created with a + length of zero. + + Despite it's name, the :meth:`format` method can not format a + blank tag to make it NDEF compatible. This is because the user + data are of a type 2 tag can not be safely determined, also + reading all memory pages until an error response yields only + the total memory size which includes an undetermined number of + special pages at the end of memory. + + It is also not possible to change the NDEF mapping version, + located in a one-time-programmable area of the tag memory. + + """ + return super(Type2Tag, self).format(version, wipe) + + def _format(self, version, wipe): + if self.ndef and self.ndef.is_writeable: + memory = self.ndef._tag_memory + offset = self.ndef._ndef_tlv_offset + memory[offset+1:offset+3] = b"\x00\xFE" + if wipe is not None: + memory_size = memory[14] * 8 + 16 + skip_bytes = self.ndef._skip_bytes + for offset in range(offset + 3, memory_size): + if offset not in skip_bytes: + memory[offset] = wipe & 0xFF + memory.synchronize() + return True + return False + + def protect(self, password=None, read_protect=False, protect_from=0): + """Protect the tag against write access, i.e. make it read-only. + + :meth:`Type2Tag.protect` switches an NFC Forum Type 2 Tag to + read-only state by setting all lock bits to 1. This operation + can not be reversed. If the tag is not an NFC Forum Tag, + i.e. it is not formatted with an NDEF Capability Container, + the :meth:`protect` method simply returns :const:`False`. + + A generic Type 2 Tag can not be protected with a password. If + the *password* argument is provided, the :meth:`protect` + method does nothing else than return :const:`False`. The + *read_protect* and *protect_from* arguments are safely + ignored. + + """ + return super(Type2Tag, self).protect( + password, read_protect, protect_from) + + def _protect(self, password, read_protect, protect_from): + if password is not None: + log.debug("this tag can not be protected with password") + return False + + if self.ndef is None: + log.debug("can not protect a non-ndef tag") + return False + + # Set the ndef capability container write flag. We must + # synchronize to have this written before lock bits are set. + tag_memory = self.ndef._tag_memory + tag_memory[15] |= 0x0F + tag_memory.synchronize() + + # Set the static lock bits. + tag_memory[10] = 0xFF + tag_memory[11] = 0xFF + + # Search for all lock control tlv and store the first lock + # byte address and the number of lock bits in lock_control. + offset = 16 + lock_control = [] + data_area_size = tag_memory[14] * 8 + while offset < data_area_size + 16: # pragma: no branch + tlv_t, tlv_l, tlv_v = read_tlv(tag_memory, offset, set()) + log.debug("tlv type {0} at offset {1}".format(tlv_t, offset)) + if tlv_t in (0x03, 0xFE, None): + break + if tlv_t == 0x01: + log.debug("lock control tlv %s", hexlify(tlv_v).decode()) + page_addr = tlv_v[0] >> 4 + byte_offs = tlv_v[0] & 0x0F + page_size = 2 ** (tlv_v[2] & 0x0F) # BytesPerPage + lock_byte_addr = page_addr * page_size + byte_offs + lock_bits_size = tlv_v[1] if tlv_v[1] > 0 else 256 + lock_control.append((lock_byte_addr, lock_bits_size)) + offset += tlv_l + 1 + (1 if tlv_l < 255 else 3) + + # If the tag has a dynamic memory layout and we did not find + # any lock control tlv, then add default dynamic lock bits. + if tag_memory[14] > 6 and len(lock_control) == 0: + # use default dynamic lock bits layout + data_area_size = tag_memory[14] * 8 + lock_byte_addr = 16 + data_area_size + lock_bits_size = (data_area_size - 48 + 7)//8 + lock_control.append((lock_byte_addr, lock_bits_size)) + + # For any lock control entry set the referenced lock bytes to + # zero and then set the lock bits to one. + log.debug("processing lock byte list {0}".format(lock_control)) + for lock_byte_addr, lock_bits_size in lock_control: + log.debug("{0} lock bits at 0x{1:02x}".format( + lock_bits_size, lock_byte_addr)) + lock_byte_size = (lock_bits_size + 7) // 8 + for i in range(lock_byte_size): + tag_memory[lock_byte_addr+i] = 0 + for i in range(lock_bits_size): + tag_memory[lock_byte_addr+(i >> 3)] |= 1 << (i & 7) + + # Synchronize to write all lock bits to the tag. + tag_memory.synchronize() + return True + + def read(self, page): + """Send a READ command to retrieve data from the tag. + + The *page* argument specifies the offset in multiples of 4 + bytes (i.e. page number 1 will return bytes 4 to 19). The data + returned is a byte array of length 16 or None if the block is + outside the readable memory range. + + Command execution errors raise :exc:`Type2TagCommandError`. + + """ + log.debug("read pages {0} to {1}".format(page, page+3)) + + data = self.transceive(bytearray([0x30, page % 256]), timeout=0.005) + + if len(data) == 1 and data[0] & 0xFA == 0x00: + log.debug("received nak response") + self.target.sel_req = self.target.sdd_res[:] + self._target = self.clf.sense(self.target) + raise Type2TagCommandError( + INVALID_PAGE_ERROR if self.target else nfc.tag.RECEIVE_ERROR) + + if len(data) != 16: + log.debug("invalid response %s", hexlify(data).decode()) + raise Type2TagCommandError(INVALID_RESPONSE_ERROR) + + return data + + def write(self, page, data): + """Send a WRITE command to store data on the tag. + + The *page* argument specifies the offset in multiples of 4 + bytes. The *data* argument must be a string or bytearray of + length 4. + + Command execution errors raise :exc:`Type2TagCommandError`. + + """ + if len(data) != 4: + raise ValueError("data must be a four byte string or array") + + log.debug("write %s to page %s", hexlify(data).decode(), page) + rsp = self.transceive(bytearray([0xA2, page % 256]) + data) + + if len(rsp) != 1: + log.debug("invalid response %s", hexlify(data).decode()) + raise Type2TagCommandError(INVALID_RESPONSE_ERROR) + + if rsp[0] != 0x0A: # NAK + log.debug("invalid page, received nak") + raise Type2TagCommandError(INVALID_PAGE_ERROR) + + return True + + def sector_select(self, sector): + """Send a SECTOR_SELECT command to switch the 1K address sector. + + The command is only send to the tag if the *sector* number is + different from the currently selected sector number (set to 0 + when the tag instance is created). If the command was + successful, the currently selected sector number is updated + and further :meth:`read` and :meth:`write` commands will be + relative to that sector. + + Command execution errors raise :exc:`Type2TagCommandError`. + + """ + if sector != self._current_sector: + log.debug("select sector {0} (pages {1} to {2})".format( + sector, sector << 10, ((sector+1) << 8) - 1)) + + sector_select_1 = b'\xC2\xFF' + sector_select_2 = pack('Bxxx', sector) + + rsp = self.transceive(sector_select_1) + if len(rsp) == 1 and rsp[0] == 0x0A: + try: + # command is passively ack'd, i.e. there's no response + # and we must make sure there's no retries attempted + self.transceive(sector_select_2, timeout=0.001, retries=0) + except Type2TagCommandError as error: + assert int(error) == TIMEOUT_ERROR # passive ack + else: + log.debug("sector {0} does not exist".format(sector)) + raise Type2TagCommandError(INVALID_SECTOR_ERROR) + else: + log.debug("sector select is not supported for this tag") + raise Type2TagCommandError(INVALID_SECTOR_ERROR) + + log.debug("sector {0} is now selected".format(sector)) + self._current_sector = sector + return self._current_sector + + def transceive(self, data, timeout=0.1, retries=2): + """Send a Type 2 Tag command and receive the response. + + :meth:`transceive` is a type 2 tag specific wrapper around the + :meth:`nfc.ContactlessFrontend.exchange` method. It can be + used to send custom commands as a sequence of *data* bytes to + the tag and receive the response data bytes. If *timeout* + seconds pass without a response, the operation is aborted and + :exc:`~nfc.tag.TagCommandError` raised with the TIMEOUT_ERROR + error code. + + Command execution errors raise :exc:`Type2TagCommandError`. + + """ + log.debug(">> {0} ({1:f}s)".format(hexlify(data).decode(), timeout)) + + if not self.target: + # Sometimes we have to (re)sense the target during + # communication. If that failed (tag gone) then any + # further attempt to transceive() is the same as + # "unrecoverable timeout error". + raise Type2TagCommandError(nfc.tag.TIMEOUT_ERROR) + + started = time.time() + error = None + for retry in range(1 + retries): + try: + data = self.clf.exchange(data, timeout) + break + except nfc.clf.CommunicationError as e: + error = e + reason = error.__class__.__name__ + log.debug("%s after %d retries" % (reason, retry)) + else: + if type(error) is nfc.clf.TimeoutError: + raise Type2TagCommandError(nfc.tag.TIMEOUT_ERROR) + if type(error) is nfc.clf.TransmissionError: + raise Type2TagCommandError(nfc.tag.RECEIVE_ERROR) + if type(error) is nfc.clf.ProtocolError: + raise Type2TagCommandError(nfc.tag.PROTOCOL_ERROR) + raise RuntimeError("unexpected " + repr(error)) + + elapsed = time.time() - started + log.debug("<< {0} ({1:f}s)".format(hexlify(data).decode(), elapsed)) + return data + + +class Type2TagMemoryReader(object): + """The memory reader provides a convenient way to read and write + :class:`Type2Tag` memory. Once instantiated with a proper type + 2 *tag* object the tag memory can then be accessed as a linear + sequence of bytes, without any considerations of sector or + page boundaries. Modified bytes can be written to tag memory + with :meth:`synchronize`. :: + + clf = nfc.ContactlessFrontend(...) + tag = clf.connect(rdwr={'on-connect': None}) + if isinstance(tag, nfc.tag.tt2.Type2Tag): + tag_memory = nfc.tag.tt2.Type2TagMemoryReader(tag) + tag_memory[16:19] = [0x03, 0x00, 0xFE] + tag_memory.synchronize() + + """ + def __init__(self, tag): + assert isinstance(tag, Type2Tag) + self._data_from_tag = bytearray() + self._data_in_cache = bytearray() + self._tag = tag + + def __len__(self): + return len(self._data_from_tag) + + def __getitem__(self, key): + if isinstance(key, slice): + start, stop, step = key.indices(0x100000) + if stop > len(self): + self._read_from_tag(stop) + elif key >= len(self): + self._read_from_tag(stop=key+1) + return self._data_in_cache[key] + + def __setitem__(self, key, value): + self.__getitem__(key) + if isinstance(key, slice): + if len(value) != len(range(*key.indices(0x100000))): + msg = "{cls} requires item assignment of identical length" + raise ValueError(msg.format(cls=self.__class__.__name__)) + self._data_in_cache[key] = value + del self._data_in_cache[len(self):] + + def __delitem__(self, key): + msg = "{cls} object does not support item deletion" + raise TypeError(msg.format(cls=self.__class__.__name__)) + + def _read_from_tag(self, stop): + index = (len(self) >> 4) << 4 + while index < stop: + self._tag.sector_select(index >> 10) + data = self._tag.read(index >> 2) + self._data_from_tag[index:] = data + self._data_in_cache[index:] = data + index += 16 + + def _write_to_tag(self, stop): + index = 0 + while index < stop: + data = self._data_in_cache[index:index+4] + if data != self._data_from_tag[index:index+4]: + self._tag.sector_select(index >> 10) + self._tag.write(index >> 2, data) + self._data_from_tag[index:index+4] = data + index += 4 + + def synchronize(self): + """Write pages that contain modified data back to tag memory.""" + self._write_to_tag(stop=len(self)) + + +def activate(clf, target): + # Type 2 Tags go mute when they receive an unsupported command. It + # is then necessary to sense again and by copying sdd_res to + # sel_req we ensure that only the same tag will be found. + target.sel_req = target.sdd_res[:] + if target.sdd_res[0] == 0x04: # NXP + import nfc.tag.tt2_nxp + tag = nfc.tag.tt2_nxp.activate(clf, target) + if tag is not None: + return tag + else: + # make sure the tag is still alive + target = clf.sense(target) + if target: + return Type2Tag(clf, target) diff --git a/src/lib/nfc/tag/tt2_nxp.py b/src/lib/nfc/tag/tt2_nxp.py new file mode 100644 index 0000000..1089990 --- /dev/null +++ b/src/lib/nfc/tag/tt2_nxp.py @@ -0,0 +1,771 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2014, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import nfc.clf +from . import tt2 + +import os +import struct +from binascii import hexlify +from pyDes import triple_des, CBC + +import logging +log = logging.getLogger(__name__) + + +class MifareUltralight(tt2.Type2Tag): + """Mifare Ultralight is a simple type 2 tag with no specific + features. It can store up to 46 byte NDEF message data. This class + does not do much more than to provide the known memory size. + + """ + def __init__(self, clf, target): + super(MifareUltralight, self).__init__(clf, target) + self._product = "Mifare Ultralight (MF01CU1)" + + def dump(self): + return super(MifareUltralight, self)._dump(stop=16) + + +class MifareUltralightC(tt2.Type2Tag): + """Mifare Ultralight C provides more memory, to store up to 142 byte + NDEF message data, and can be password protected. + + """ + class NDEF(tt2.Type2Tag.NDEF): + def _read_capability_data(self, tag_memory): + base_class = super(MifareUltralightC.NDEF, self) + if base_class._read_capability_data(tag_memory): + if self.tag.is_authenticated: + if not self._readable and tag_memory[15] >> 4 == 8: + self._readable = True + if not self._writeable and tag_memory[15] & 0xF == 8: + self._writeable = bool(tag_memory[10:12] == b"\0\0") + return True + return False + + def __init__(self, clf, target): + super(MifareUltralightC, self).__init__(clf, target) + self._product = "Mifare Ultralight C (MF01CU2)" + + def dump(self): + lines = super(MifareUltralightC, self)._dump(stop=40) + + footer = dict(zip(range(40, 44), ( + "LOCK2-LOCK3", "CTR0-CTR1", "AUTH0", "AUTH1"))) + + for i in sorted(footer.keys()): + try: + data = self.read(i)[0:4] + except tt2.Type2TagCommandError: + data = [None, None, None, None] + lines.append(tt2.pagedump(i, data, footer[i])) + + return lines + + def protect(self, password=None, read_protect=False, protect_from=0): + """Protect a Mifare Ultralight C Tag. + + A Mifare Ultrlight C Tag can be provisioned with a custom + password (or the default manufacturer key if the password is + an empty string or bytearray). + + A non-empty *password* must provide at least 128 bit key + material, in other words it must be a string or bytearray of + length 16 or more. + + If *password* is not None, the first protected memory page can + be specified with the *protect_from* integer argument. A + memory page is 4 byte and the total number of pages is 48. A + *protect_from* argument of 48 effectively disables memory + protection. A *protect_from* argument of 3 protects all user + data pages including the bitwise one-time-programmable page + 3. Any value less than 3 or more than 48 is accepted but to + the same effect as if 3 or 48 were specified. If effective + protection starts at page 3 and the tag is formatted for NDEF, + the :meth:`protect` method does also modify the NDEF + read/write capability byte. + + If *password* is not None and *read_protect* is True then the + tag memory content will also be protected against read access, + i.e. successful authentication will be required to read + protected pages. + + The :meth:`protect` method verifies a password change by + authenticating with the new *password* after all modifications + were made and returns the result of :meth:`authenticate`. + + .. warning:: If protect is called without a password, the + default Type 2 Tag protection method will set the lock + bits to readonly. This process is not reversible. + + """ + args = (password, read_protect, protect_from) + return super(MifareUltralightC, self).protect(*args) + + def _protect(self, password, read_protect, protect_from): + if password is None: + return self._protect_with_lockbits() + else: + args = (password, read_protect, protect_from) + return self._protect_with_password(*args) + + def _protect_with_lockbits(self): + try: + ndef_cc = self.read(3)[0:4] + if ndef_cc[0] == 0xE1 and ndef_cc[1] >> 4 == 1: + ndef_cc[3] = 0x0F + self.write(3, ndef_cc) + self.write(2, b"\x00\x00\xFF\xFF") + self.write(40, b"\xFF\xFF\x00\x00") + return True + except tt2.Type2TagCommandError: + return False + + def _protect_with_password(self, password, read_protect, protect_from): + if password and len(password) < 16: + raise ValueError("password must be at least 16 byte") + + # The first 16 password character bytes are taken as key + # unless the password is empty. If it's empty we use the + # factory default password. + key = password[0:16] if password != b"" else b"IEMKAERB!NACUOYF" + log.debug("protect with key %s", hexlify(key).decode()) + + # split the key and reverse + key1, key2 = key[7::-1], key[15:7:-1] + self.write(44, key1[0:4]) + self.write(45, key1[4:8]) + self.write(46, key2[0:4]) + self.write(47, key2[4:8]) + + # protect from memory page + self.write(42, bytearray([max(3, min(protect_from, 0x30))]) + + b"\0\0\0") + + # set read protection flag + self.write(43, b"\0\0\0\0" if read_protect else b"\x01\0\0\0") + + # Set NDEF read/write permissions if protection starts at page + # 3 and the tag is formatted for NDEF. We set the read/write + # permission flags to 8, thus indicating proprietary access. + if protect_from <= 3: + ndef_cc = self.read(3)[0:4] + if ndef_cc[0] == 0xE1 and ndef_cc[1] & 0xF0 == 0x10: + ndef_cc[3] |= (0x88 if read_protect else 0x08) + self.write(3, ndef_cc) + + # Reactivate the tag to have the key effective and + # authenticate with the same key + self._target = self.clf.sense(self.target) + return self.authenticate(key) if self.target else False + + def authenticate(self, password): + """Authenticate with a Mifare Ultralight C Tag. + + :meth:`autenticate` executes the Mifare Ultralight C mutual + authentication protocol to verify that the *password* argument + matches the key that is stored in the card. A new card key can + be set with :meth:`protect`. + + The *password* argument must be a string with either 0 or at + least 16 bytes. A zero length password string indicates that + the factory default card key be used. From a password with 16 + or more bytes the first 16 byte are taken as card key, + remaining bytes are ignored. A password length between 1 and + 15 generates a ValueError exception. + + The authentication result is True if the password was + confirmed and False if not. + + """ + return super(MifareUltralightC, self).authenticate(password) + + def _authenticate(self, password): + # The first 16 password character bytes are taken as key + # unless the password is empty. If it's empty we use the + # factory default password. + key = password[0:16] if password != b"" else b"IEMKAERB!NACUOYF" + + if len(key) != 16: + raise ValueError("password must be at least 16 byte") + + log.debug("authenticate with key %s", hexlify(key).decode()) + + rsp = self.transceive(b"\x1A\x00") + m1 = bytes(rsp[1:9]) + iv = b"\x00\x00\x00\x00\x00\x00\x00\x00" + rb = triple_des(key, CBC, iv).decrypt(m1) + + log.debug("received challenge") + log.debug("iv = %s", hexlify(iv).decode()) + log.debug("m1 = %s", hexlify(m1).decode()) + log.debug("rb = %s", hexlify(rb).decode()) + + ra = os.urandom(8) + iv = bytes(rsp[1:9]) + + m2 = triple_des(key, CBC, iv).encrypt(ra + rb[1:8] + ( + struct.pack("B", rb[0]) if isinstance(rb[0], int) else rb[0])) + + log.debug("sending response") + log.debug("ra = %s", hexlify(ra).decode()) + log.debug("iv = %s", hexlify(iv).decode()) + log.debug("m2 = %s", hexlify(m2).decode()) + try: + rsp = self.transceive(b"\xAF" + m2) + except tt2.Type2TagCommandError: + return False + + m3 = bytes(rsp[1:9]) + iv = m2[8:16] + log.debug("received confirmation") + log.debug("iv = %s", hexlify(iv).decode()) + log.debug("m3 = %s", hexlify(m3).decode()) + + return triple_des(key, CBC, iv).decrypt(m3) == ra[1:9] \ + + (struct.pack("B", ra[0]) if isinstance(ra[0], int) else ra[0]) + + +class NTAG203(tt2.Type2Tag): + """The NTAG203 is a plain memory Tag with 144 bytes user data memory + plus a 16-bit one-way counter. It does not have any security + features beyond the standard lock bit mechanism that permanently + disables write access. + + """ + def __init__(self, clf, target): + super(NTAG203, self).__init__(clf, target) + self._product = "NXP NTAG203" + + def dump(self): + lines = super(NTAG203, self)._dump(40) + + footer = dict(zip(range(40, 42), ("LOCK2-LOCK3", "CNTR0-CNTR1"))) + + for i in sorted(footer.keys()): + try: + data = self.read(i)[0:4] + except tt2.Type2TagCommandError: + data = [None, None, None, None] + lines.append(tt2.pagedump(i, data, footer[i])) + + return lines + + def protect(self, password=None, read_protect=False, protect_from=0): + """Set lock bits to disable future memory modifications. + + If *password* is None, all memory pages except the 16-bit + counter in page 41 are protected by setting the relevant lock + bits (note that lock bits can not be reset). If valid NDEF + management data is found in page 4, protect() also sets the + NDEF write flag to read-only. + + The NTAG203 can not be password protected. If a *password* + argument is provided, the protect() method always returns + False. + + """ + return super(NTAG203, self).protect( + password, read_protect, protect_from) + + def _protect(self, password, read_protect, protect_from): + if password is None: + try: + ndef_cc = self.read(3)[0:4] + if ndef_cc[0] == 0xE1 and ndef_cc[1] >> 4 == 1: + ndef_cc[3] = 0x0F + self.write(3, ndef_cc) + self.write(2, b"\x00\x00\xFF\xFF") + self.write(40, b"\xFF\x01\x00\x00") + return True + except tt2.Type2TagCommandError: + pass + return False + + def _format(self, version, wipe): + if self.ndef is None: + log.debug("no management data, writing factory defaults") + self.write(4, b'\x01\x03\xA0\x10') + self.write(5, b'\x44\x03\x00\xFE') + return super(NTAG203, self)._format(version, wipe) + + +class NTAG21x(tt2.Type2Tag): + """Base class for the NTAG21x family (210/212/213/215/216). The + methods and attributes documented here are supported for all + NTAG21x products. + + All NTAG21x products support a simple password protection scheme + that can be configured to restrict write as well as read access to + memory starting from a selected page address. A factory programmed + ECC signature allows to verify the tag unique identifier. + + """ + class NDEF(tt2.Type2Tag.NDEF): + def _read_capability_data(self, tag_memory): + if super(NTAG21x.NDEF, self)._read_capability_data(tag_memory): + if self.tag.is_authenticated: + if not self._readable and tag_memory[15] >> 4 == 8: + self._readable = True + if not self._writeable and tag_memory[15] & 0xF == 8: + self._writeable = bool(tag_memory[10:12] == b"\0\0") + return True + return False + + @property + def signature(self): + """The 32-byte ECC tag signature programmed at chip production. The + signature is provided as a string and can only be read. + + The signature attribute is always loaded from the tag when it + is accessed, i.e. it is not cached. If communication with the + tag fails for some reason the signature attribute is set to a + 32-byte string of all zeros. + + """ + log.debug("read tag signature") + try: + return bytes(self.transceive(b"\x3C\x00")) + except tt2.Type2TagCommandError: + return 32 * b"\0" + + def protect(self, password=None, read_protect=False, protect_from=0): + """Set password protection or permanent lock bits. + + If the *password* argument is None, all memory pages will be + protected by setting the relevant lock bits (note that lock + bits can not be reset). If valid NDEF management data is + found, protect() also sets the NDEF write flag to read-only. + + All Tags of the NTAG21x family can alternatively be protected + by password. If a *password* argument is provided, the + protect() method writes the first 4 byte of the *password* + string into the Tag's password (PWD) memory bytes and the + following 2 byte of the *password* string into the password + acknowledge (PACK) memory bytes. Factory default values are + used if the *password* argument is an empty string. Lock bits + are not set for password protection. + + The *read_protect* and *protect_from* arguments are only + evaluated if *password* is not None. If *read_protect* is + True, the memory protection bit (PROT) is set to require + password verification also for reading of protected memory + pages. The value of *protect_from* determines the first + password protected memory page (one page is 4 byte) with the + exception that the smallest set value is page 3 even if + *protect_from* is smaller. + + """ + args = (password, read_protect, protect_from) + return super(NTAG21x, self).protect(*args) + + def _protect(self, password, read_protect, protect_from): + if password is None: + return self._protect_with_lockbits() + else: + args = (password, read_protect, protect_from) + return self._protect_with_password(*args) + + def _protect_with_lockbits(self): + try: + ndef_cc = self.read(3)[0:4] + if ndef_cc[0] == 0xE1 and ndef_cc[1] >> 4 == 1: + ndef_cc[3] = 0x0F + self.write(3, ndef_cc) + self.write(2, b"\x00\x00\xFF\xFF") + if self._cfgpage > 16: + self.write(self._cfgpage - 1, b"\xFF\xFF\xFF\x00") + cfgdata = self.read(self._cfgpage) + if cfgdata[4] & 0x40 == 0: + cfgdata[4] |= 0x40 # set CFGLCK bit + self.write(self._cfgpage + 1, cfgdata[4:8]) + return True + except tt2.Type2TagCommandError: + return False + + def _protect_with_password(self, password, read_protect, protect_from): + if password and len(password) < 6: + raise ValueError("password must be at least 6 bytes") + + key = password[0:6] if password != b"" else b"\xFF\xFF\xFF\xFF\0\0" + log.debug("protect with key %s", hexlify(key).decode()) + + # read CFG0, CFG1, PWD and PACK + cfg = self.read(self._cfgpage) + + # set password and acknowledge + cfg[8:14] = key + + # start protection from page + cfg[3] = max(3, min(protect_from, 255)) + + # set read protection bit + cfg[4] = cfg[4] | 0x80 if read_protect else cfg[4] & 0x7F + + # write configuration to tag + for i in range(4): + self.write(self._cfgpage + i, cfg[i*4:(i+1)*4]) + + # Set NDEF read/write permissions if protection starts at page + # 3 and the tag is formatted for NDEF. We set the read/write + # permission flags to 8, thus indicating proprietary access. + if protect_from <= 3: + ndef_cc = self.read(3)[0:4] + if ndef_cc[0] == 0xE1 and ndef_cc[1] & 0xF0 == 0x10: + ndef_cc[3] |= (0x88 if read_protect else 0x08) + self.write(3, ndef_cc) + + # Reactivate the tag to have the key effective and + # authenticate with the same key + self._target = self.clf.sense(self.target) + return self.authenticate(key) if self.target else False + + def authenticate(self, password): + """Authenticate with password to access protected memory. + + An NTAG21x implements a simple password protection scheme. The + reader proofs possession of a share secret by sending a 4-byte + password and the tag proofs possession of a shared secret by + returning a 2-byte password acknowledge. Because password and + password acknowledge are transmitted in plain text special + considerations should be given to under which conditions + authentication is performed. If, for example, an attacker is + able to mount a relay attack both secret values are easily + lost. + + The *password* argument must be a string of length zero or at + least 6 byte characters. If the *password* length is zero, + authentication is performed with factory default values. If + the *password* contains at least 6 bytes, the first 4 byte are + send to the tag as the password secret and the following 2 + byte are compared against the password acknowledge that is + received from the tag. + + The authentication result is True if the password was + confirmed and False if not. + + """ + return super(NTAG21x, self).authenticate(password) + + def _authenticate(self, password): + if password and len(password) < 6: + raise ValueError("password must be at least 6 bytes") + + key = password[0:6] if password != b"" else b"\xFF\xFF\xFF\xFF\0\0" + log.debug("authenticate with key %s", hexlify(key).decode()) + + try: + rsp = self.transceive(b"\x1B" + key[0:4]) + return rsp == key[4:6] + except tt2.Type2TagCommandError: + return False + + def _dump(self, stop, footer): + lines = super(NTAG21x, self)._dump(stop) + for i in sorted(footer.keys()): + try: + data = self.read(i)[0:4] + except tt2.Type2TagCommandError: + data = [None, None, None, None] + lines.append(tt2.pagedump(i, data, footer[i])) + return lines + + +class NTAG210(NTAG21x): + """The NTAG210 provides 48 bytes user data memory, password + protection, originality signature and a UID mirror function. + + """ + def __init__(self, clf, target): + super(NTAG210, self).__init__(clf, target) + self._product = "NXP NTAG210" + self._cfgpage = 16 + + def _format(self, version, wipe): + if self.ndef is None: + log.debug("no management data, writing factory defaults") + self.write(4, b'\x03\x00\xFE\x00') + self.write(5, b'\x00\x00\x00\x00') + return super(NTAG210, self)._format(version, wipe) + + def dump(self): + footer = dict(zip(range(16, 20), + ("MIRROR_BYTE, RFU, MIRROR_PAGE, AUTH0", + "ACCESS", "PWD0-PWD3", "PACK0-PACK1"))) + return super(NTAG210, self)._dump(16, footer) + + +class NTAG212(NTAG21x): + """The NTAG212 provides 128 bytes user data memory, password + protection, originality signature and a UID mirror function. + + """ + def __init__(self, clf, target): + super(NTAG212, self).__init__(clf, target) + self._product = "NXP NTAG212" + self._cfgpage = 37 + + def _format(self, version, wipe): + if self.ndef is None: + log.debug("no management data, writing factory defaults") + self.write(4, b'\x01\x03\x90\x0A') + self.write(5, b'\x34\x03\x00\xFE') + return super(NTAG212, self)._format(version, wipe) + + def dump(self): + text = ("LOCK2-LOCK4", "MIRROR_BYTE, RFU, MIRROR_PAGE, AUTH0", + "ACCESS", "PWD0-PWD3", "PACK0-PACK1") + footer = dict(zip(range(36, 36+len(text)), text)) + return super(NTAG212, self)._dump(36, footer) + + +class NTAG213(NTAG21x): + """The NTAG213 provides 144 bytes user data memory, password + protection, originality signature, a tag read counter and a mirror + function for the tag unique identifier and the read counter. + + """ + def __init__(self, clf, target): + super(NTAG213, self).__init__(clf, target) + self._product = "NXP NTAG213" + self._cfgpage = 41 + + def _format(self, version, wipe): + if self.ndef is None: + log.debug("no management data, writing factory defaults") + self.write(4, b'\x01\x03\xA0\x0C') + self.write(5, b'\x34\x03\x00\xFE') + return super(NTAG213, self)._format(version, wipe) + + def dump(self): + text = ("LOCK2-LOCK4", "MIRROR, RFU, MIRROR_PAGE, AUTH0", + "ACCESS", "PWD0-PWD3", "PACK0-PACK1") + footer = dict(zip(range(40, 40+len(text)), text)) + return super(NTAG213, self)._dump(40, footer) + + +class NTAG215(NTAG21x): + """The NTAG215 provides 504 bytes user data memory, password + protection, originality signature, a tag read counter and a mirror + function for the tag unique identifier and the read counter. + + """ + def __init__(self, clf, target): + super(NTAG215, self).__init__(clf, target) + self._product = "NXP NTAG215" + self._cfgpage = 131 + + def _format(self, version, wipe): + if self.ndef is None: + log.debug("no management data, writing factory defaults") + self.write(4, b'\x03\x00\xFE\x00') + self.write(5, b'\x00\x00\x00\x00') + return super(NTAG215, self)._format(version, wipe) + + def dump(self): + text = ("LOCK2-LOCK4", "MIRROR, RFU, MIRROR_PAGE, AUTH0", + "ACCESS", "PWD0-PWD3", "PACK0-PACK1") + footer = dict(zip(range(130, 130+len(text)), text)) + return super(NTAG215, self)._dump(130, footer) + + +class NTAG216(NTAG21x): + """The NTAG216 provides 888 bytes user data memory, password + protection, originality signature, a tag read counter and a mirror + function for the tag unique identifier and the read counter. + + """ + def __init__(self, clf, target): + super(NTAG216, self).__init__(clf, target) + self._product = "NXP NTAG216" + self._cfgpage = 227 + + def _format(self, version, wipe): + if self.ndef is None: + log.debug("no management data, writing factory defaults") + self.write(4, b'\x03\x00\xFE\x00') + self.write(5, b'\x00\x00\x00\x00') + return super(NTAG216, self)._format(version, wipe) + + def dump(self): + text = ("LOCK2-LOCK4", "MIRROR, RFU, MIRROR_PAGE, AUTH0", + "ACCESS", "PWD0-PWD3", "PACK0-PACK1") + footer = dict(zip(range(226, 226+len(text)), text)) + return super(NTAG216, self)._dump(226, footer) + + +class MifareUltralightEV1(NTAG21x): + """Mifare Ultralight EV1 + + """ + def __init__(self, clf, target, product): + super(MifareUltralightEV1, self).__init__(clf, target) + self._product = "Mifare Ultralight EV1 ({0})".format(product) + + def _dump_ul11(self): + text = ("MOD, RFU, RFU, AUTH0", "ACCESS, VCTID, RFU, RFU", + "PWD0, PWD1, PWD2, PWD3", "PACK0, PACK1, RFU, RFU") + footer = dict(zip(range(16, 16+len(text)), text)) + return super(MifareUltralightEV1, self)._dump(16, footer) + + def _dump_ul21(self): + text = ("LOCK2, LOCK3, LOCK4, RFU", + "MOD, RFU, RFU, AUTH0", "ACCESS, VCTID, RFU, RFU", + "PWD0, PWD1, PWD2, PWD3", "PACK0, PACK1, RFU, RFU") + footer = dict(zip(range(36, 36+len(text)), text)) + return super(MifareUltralightEV1, self)._dump(36, footer) + + +class MF0UL11(MifareUltralightEV1): + def __init__(self, clf, target): + super(MF0UL11, self).__init__(clf, target, "MF0UL11") + + def dump(self): + return self._dump_ul11() + + +class MF0ULH11(MifareUltralightEV1): + def __init__(self, clf, target): + super(MF0ULH11, self).__init__(clf, target, "MF0ULH11") + + def dump(self): + return self._dump_ul11() + + +class MF0UL21(MifareUltralightEV1): + def __init__(self, clf, target): + super(MF0UL21, self).__init__(clf, target, "MF0UL21") + + def dump(self): + return self._dump_ul21() + + +class MF0ULH21(MifareUltralightEV1): + def __init__(self, clf, target): + super(MF0ULH21, self).__init__(clf, target, "MF0ULH21") + + def dump(self): + return self._dump_ul21() + + +class NTAGI2C(tt2.Type2Tag): + def _dump(self, stop): + s = super(NTAGI2C, self)._dump(stop) + + data = self.read(stop)[0:4] + s.append(tt2.pagedump(stop, data, "LOCK2-LOCK4, CHK")) + + data = self.read(232) + s.append("") + s.append("Configuration registers:") + s.append(tt2.pagedump(stop & 256 | 232, data[0:4], + "NC, LD, SM, WDT0")) + s.append(tt2.pagedump(stop & 256 | 233, data[4:8], + "WDT1, CLK, LOCK, RFU")) + + self.sector_select(3) + data = self.read(248) + s.append("") + s.append("Session registers:") + s.append(tt2.pagedump(0x3F8, data[0:4], "NC, LD, SM, WDT0")) + s.append(tt2.pagedump(0x3F9, data[4:8], "WDT1, CLK, NS, RFU")) + + self.sector_select(0) + return s + + +class NT3H1101(NTAGI2C): + """NTAG I2C 1K. + + """ + def __init__(self, clf, target): + super(NT3H1101, self).__init__(clf, target) + self._product = "NTAG I2C 1K (NT3H1101)" + + def dump(self): + return super(NT3H1101, self)._dump(226) + + +class NT3H1201(NTAGI2C): + """NTAG I2C 2K. + + """ + def __init__(self, clf, target): + super(NT3H1201, self).__init__(clf, target) + self._product = "NTAG I2C 2K (NT3H1201)" + + def dump(self): + return super(NT3H1201, self)._dump(480) + + +VERSION_MAP = { + b"\x00\x04\x03\x01\x01\x00\x0B\x03": MF0UL11, + b"\x00\x04\x03\x02\x01\x00\x0B\x03": MF0ULH11, + b"\x00\x04\x03\x01\x01\x00\x0E\x03": MF0UL21, + b"\x00\x04\x03\x02\x01\x00\x0E\x03": MF0ULH21, + b"\x00\x04\x04\x01\x01\x00\x0B\x03": NTAG210, + b"\x00\x04\x04\x01\x01\x00\x0E\x03": NTAG212, + b"\x00\x04\x04\x02\x01\x00\x0F\x03": NTAG213, + b"\x00\x04\x04\x02\x01\x00\x11\x03": NTAG215, + b"\x00\x04\x04\x02\x01\x00\x13\x03": NTAG216, + b"\x00\x04\x04\x05\x02\x01\x13\x03": NT3H1101, + b"\x00\x04\x04\x05\x02\x01\x15\x03": NT3H1201, + # b"\x00\x04\x04\x05\x02\x02\x13\x03": NT3H2111, + # b"\x00\x04\x04\x05\x02\x02\x15\x03": NT3H2211, +} + + +def activate(clf, target): + log.debug("check if authenticate command is available") + try: + rsp = clf.exchange(b'\x1A\x00', timeout=0.01) + if clf.sense(target) is None: + return + if rsp.startswith(b"\xAF"): + return MifareUltralightC(clf, target) + except nfc.clf.TimeoutError: + if clf.sense(target) is None: + return + except nfc.clf.CommunicationError as error: + log.debug(repr(error)) + return + + log.debug("check if version command is available") + try: + rsp = bytes(clf.exchange(b'\x60', timeout=0.01)) + if rsp in VERSION_MAP: + return VERSION_MAP[rsp](clf, target) + if rsp == b"\x00": + if clf.sense(target) is None: + return None + else: + return NTAG203(clf, target) + log.debug("no match for version %s", hexlify(rsp).decode().upper()) + return + except nfc.clf.TimeoutError: + if clf.sense(target) is None: + return + except nfc.clf.CommunicationError as error: + log.debug(repr(error)) + return + + return MifareUltralight(clf, target) diff --git a/src/lib/nfc/tag/tt3.py b/src/lib/nfc/tag/tt3.py new file mode 100644 index 0000000..d77a5f9 --- /dev/null +++ b/src/lib/nfc/tag/tt3.py @@ -0,0 +1,930 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import nfc.tag +import nfc.clf + +import math +import time +import itertools +from binascii import hexlify +from struct import pack, unpack + +import logging +log = logging.getLogger(__name__) + + +RSP_LENGTH_ERROR, RSP_CODE_ERROR, TAG_IDM_ERROR, DATA_SIZE_ERROR = range(1, 5) + + +class Type3TagCommandError(nfc.tag.TagCommandError): + errno_str = { + RSP_LENGTH_ERROR: "invalid response length", + RSP_CODE_ERROR: "invalid response code", + TAG_IDM_ERROR: "answer from wrong tag", + DATA_SIZE_ERROR: "insufficient data received", + # FeliCa Lite specific error codes + 0x01A6: "invalid service code number or attribute", + 0x01B1: "authentication required to read (first block in list)", + 0x02B1: "authentication required to read (second block in list)", + 0x04B1: "authentication required to read (third block in list)", + 0x08B1: "authentication required to read (fourth block in list)", + 0x02B2: "verification failure for write with mac operation", + } + + +class ServiceCode: + """A service code provides access to a group of data blocks located on + the card file system. A service code is a 16-bit structure + composed of a 10-bit service number and a 6-bit service + attribute. The service attribute determines the service type and + whether authentication is required. + + """ + def __init__(self, number, attribute): + self.number = number + self.attribute = attribute + + def __repr__(self): + return "ServiceCode({0}, {1})".format(self.number, self.attribute) + + def __str__(self): + attribute_map = { + 0b001000: "Random RW with key", + 0b001001: "Random RW w/o key", + 0b001010: "Random RO with key", + 0b001011: "Random RO w/o key", + 0b001100: "Cyclic RW with key", + 0b001101: "Cyclic RW w/o key", + 0b001110: "Cyclic RO with key", + 0b001111: "Cyclic RO w/o key", + 0b010000: "Purse Direct with key", + 0b010001: "Purse Direct w/o key", + 0b010010: "Purse Cashback with key", + 0b010011: "Purse Cashback w/o key", + 0b010100: "Purse Decrement with key", + 0b010101: "Purse Decrement w/o key", + 0b010110: "Purse Read Only with key", + 0b010111: "Purse Read Only w/o key", + } + try: + attribute_string = attribute_map[self.attribute] + except KeyError: + attribute_string = "Type {0:06b}b".format(self.attribute) + return "Service Code {0:04X}h (Service {1} {2!s})".format( + int(self), self.number, attribute_string) + + def __int__(self): + return self.number << 6 | self.attribute + + def pack(self): + """Pack the service code for transmission. Returns a 2 byte string.""" + sn, sa = self.number, self.attribute + return pack("> 6, v & 0x3f) + + +class BlockCode: + """A block code indicates a data block within a service. A block code + is a 16-bit or 24-bit structure composed of a length bit (1b if + the block number is less than 256), a 3-bit access mode, a 4-bit + service list index and an 8-bit or 16-bit block number. + + """ + def __init__(self, number, access=0, service=0): + self.number = number + self.access = access + self.service = service + + def __repr__(self): + return "BlockCode({0}, {1}, {2})".format( + self.number, self.access, self.service) + + def __str__(self): + s = "BlockCode(number={0}, access={1:03b}, service={2})" + return s.format(self.number, self.access, self.service) + + def __bytes__(self): + return str(self).encode() + + def pack(self): + """Pack the block code for transmission. Returns a 2-3 byte string.""" + bn, am, sx = self.number, self.access, self.service + return bytes( + bytearray([bool(bn < 256) << 7 | (am & 0x7) << 4 | (sx & 0xf)]) + + (bytearray([bn]) if bn < 256 else pack("H", data[14:16])[0]: + log.debug("ndef attribute data checksum error") + return None + + ver, nbr, nbw, nmaxb = unpack(">BBBH", data[0:5]) + writef, rwflag = unpack(">BB", data[9:11]) + length = unpack(">I", b"\x00" + data[11:14])[0] + self._capacity = nmaxb * 16 + self._writeable = rwflag != 0 and nbw > 0 + self._readable = writef == 0 and nbr > 0 + attributes = { + 'ver': ver, 'nbr': nbr, 'nbw': nbw, 'nmaxb': nmaxb, + 'writef': writef, 'rwflag': rwflag, 'ln': length} + log.debug("got ndef attributes {0}".format(attributes)) + return attributes + + def _write_attribute_data(self, attributes): + log.debug("set ndef attributes {0}".format(attributes)) + attribute_data = bytearray(16) + attribute_data[0] = attributes['ver'] + attribute_data[1] = attributes['nbr'] + attribute_data[2] = attributes['nbw'] + attribute_data[3:5] = pack('>H', attributes['nmaxb']) + attribute_data[9] = attributes['writef'] + attribute_data[10] = attributes['rwflag'] + attribute_data[11:14] = pack('>I', attributes['ln'])[1:4] + attribute_data[14:16] = pack('>H', sum(attribute_data[0:14])) + self._tag.write_to_ndef_service(attribute_data, 0) + + def _read_ndef_data(self): + if self.tag.sys != 0x12FC: + try: + self.tag.idm, self.tag.pmm = self._tag.polling(0x12FC) + self.tag.sys = 0x12FC + except Type3TagCommandError: + return None + + attributes = self._read_attribute_data() + if attributes is None: + log.debug("found no attribute data (maybe checksum error)") + return None + if attributes['ver'] >> 4 != 1: + log.debug("unsupported ndef mapping major version") + return None + + last_block_number = 1 + (attributes['ln'] + 15) // 16 + data = bytearray() + + for i in range(1, last_block_number, attributes['nbr']): + last_block = min(i + attributes['nbr'], last_block_number) + block_list = range(i, last_block) + try: + data += self.tag.read_from_ndef_service(*block_list) + except Type3TagCommandError: + return None + + data = data[0:attributes['ln']] + log.debug("got {0} byte ndef data {1}{2}".format( + len(data), + hexlify(data[0:32]).decode(), + ('', '...')[len(data) > 32])) + + return data + + def _write_ndef_data(self, data): + attributes = self._read_attribute_data() + attributes['writef'] = 0x0F + self._write_attribute_data(attributes) + + log.debug("set {0} byte ndef data {1}{2}".format( + len(data), + hexlify(data[0:32]).decode(), + ('', '...')[len(data) > 32])) + + last_block_number = 1 + (len(data) + 15) // 16 + attributes['ln'] = len(data) # because we may need to pad zeros + data = data + bytearray(-len(data) % 16) # adjust to block size + + for i in range(1, last_block_number, attributes['nbw']): + last_block = min(i + attributes['nbw'], last_block_number) + block_data = data[(i-1)*16:(last_block-1)*16] + self._tag.write_to_ndef_service( + block_data, *range(i, last_block)) + + attributes['writef'] = 0x00 + self._write_attribute_data(attributes) + return True + + def __init__(self, clf, target): + super(Type3Tag, self).__init__(clf, target) + self.idm = target.sensf_res[1:9] + self.pmm = target.sensf_res[9:17] + self.sys = 0xFFFF + if len(target.sensf_res) > 17: + self.sys = unpack(">H", target.sensf_res[17:19])[0] + self._nfcid = bytearray(self.idm) + + def __str__(self): + s = " PMM={pmm} SYS={sys:04X}" + return nfc.tag.Tag.__str__(self) + s.format( + pmm=hexlify(self.pmm).decode().upper(), sys=self.sys) + + def _is_present(self): + # Check if the card still responds to the acquired system code + # and the returned identifier (IDm) matches. This is called + # from nfc.tag.Tag for the 'is_present' attribute. + try: + idm, pmm = self.polling(self.sys) + return idm == self.identifier + except Type3TagCommandError: + return False + + def dump(self): + """Read all data blocks of an NFC Forum Tag. + + For an NFC Forum Tag (system code 0x12FC) :meth:`dump` reads + all data blocks from service 0x000B (NDEF read service) and + returns a list of strings suitable for printing. The number of + strings returned does not necessarily reflect the number of + data blocks because a range of data blocks with equal content + is reduced to fewer lines of output. + + """ + if self.sys == 0x12FC: + ndef_read_service = ServiceCode(0, 0b01011) + return self.dump_service(ndef_read_service) + else: + return ["This is not an NFC Forum Tag."] + + def dump_service(self, sc): + """Read all data blocks of a given service. + + :meth:`dump_service` reads all data blocks from the service + with service code *sc* and returns a list of strings suitable + for printing. The number of strings returned does not + necessarily reflect the number of data blocks because a range + of data blocks with equal content is reduced to fewer lines of + output. + + """ + def lprint(fmt, data, index): + ispchr = lambda x: x >= 32 and x <= 126 # noqa: E731 + + def print_bytes(octets): + return ' '.join(['%02x' % x for x in octets]) + + def print_chars(octets): + return ''.join([chr(x) if ispchr(x) else '.' for x in octets]) + + return fmt.format(index, print_bytes(data), print_chars(data)) + + data_line_fmt = "{0:04X}: {1} |{2}|" + same_line_fmt = "{0:<4s} {1} |{2}|" + + lines = list() + last_data = None + same_data = 0 + + for i in itertools.count(): # pragma: no branch + assert i < 0x10000 + try: + this_data = self.read_without_encryption([sc], [BlockCode(i)]) + except Type3TagCommandError: + i = i - 1 + break + + if this_data == last_data: + same_data += 1 + else: + if same_data > 1: + lines.append(lprint(same_line_fmt, last_data, "*")) + lines.append(lprint(data_line_fmt, this_data, i)) + last_data = this_data + same_data = 0 + + if same_data > 1: + lines.append(lprint(same_line_fmt, last_data, "*")) + if same_data > 0: + lines.append(lprint(data_line_fmt, this_data, i)) + + return lines + + def format(self, version=None, wipe=None): + """Format and blank an NFC Forum Type 3 Tag. + + A generic NFC Forum Type 3 Tag can be (re)formatted if it is + in either one of blank, initialized or readwrite state. By + formatting, all contents of the attribute information block is + overwritten with values determined. The number of user data + blocks is determined by reading all memory until an error + response. Similarily, the maximum number of data block that + can be read or written with a single command is determined by + sending successively increased read and write commands. The + current data length is set to zero. The NDEF mapping version + is set to the latest known version number (1.0), unless the + *version* argument is provided and it's major version number + corresponds to one of the known major version numbers. + + By default, no data other than the attribute block is + modified. To overwrite user data the *wipe* argument must be + set to an integer value. The lower 8 bits of that value are + written to all data bytes that follow the attribute block. + + """ + return super(Type3Tag, self).format(version, wipe) + + def _format(self, version, wipe): + assert version is None or type(version) is int + assert wipe is None or type(wipe) is int + + if self.sys != 0x12FC: + log.warning("not an ndef tag and can not be made compatible") + return False + if version and version >> 4 != 1: + log.warning("Type 3 Tag NDEF mapping major version must be 1") + return False + + try: + self.read_from_ndef_service(0) + except Type3TagCommandError: + log.warning("this tag does not have any usable data blocks") + return False + + # To determine the total number of data blocks we start with + # the assumption that it must be between 0 and 2**16, then try + # reading in the middle and adjust the range depending on + # whether the read was successful or not. So in each round we + # have the smallest number that worked and the largest number + # that didn't, obviously the end is when that difference is 1. + """ + nmaxb = [0, 0x10000] + while nmaxb[1] - nmaxb[0] > 1: + block = nmaxb[0] + (nmaxb[1] - nmaxb[0]) // 2 - 1 + try: + self.read_from_ndef_service(block) + except Type3TagCommandError: + nmaxb[1] = block + 1 + else: + nmaxb[0] = block + 1 + """ + nmaxb = [0, 0x10000] + while nmaxb[1] - nmaxb[0] > 1: + print(nmaxb) + block = nmaxb[0] + (nmaxb[1] - nmaxb[0]) // 2 + try: + self.read_from_ndef_service(block) + except Type3TagCommandError: + nmaxb[1] = block + else: + nmaxb[0] = block + + nmaxb = nmaxb[0] + + # To get the number of blocks that can be read in one command + # we just try to read with an increasing number of blocks. + for nbr in range(1, 16): + try: + self.read_from_ndef_service(*(nbr*[0])) + except Type3TagCommandError: + nbr -= 1 + break + + # To get the number of blocks that can be written in one + # command we do essentially the same as for nbr, just that to + # preserve existing data we first read and then write it back. + data = self.read_from_ndef_service(0) + for nbw in range(1, 14): + try: + self.write_to_ndef_service(nbw*data, *(nbw*[0])) + except Type3TagCommandError: + nbw -= 1 + break + + # Tags with more than 4K memory require 3-byte block number + # format. This reduces the maximum number of blocks in write. + if nbw == 13 and nmaxb > 255: + nbw = 12 + + # We now have all information needed to create and write the + # new attribute data to block number 0. + attribute_data = bytearray(16) + attribute_data[0:5] = pack(">BBBH", version, nbr, nbw, nmaxb) + attribute_data[10] = 0x01 if nbw > 0 else 0x00 + attribute_data[14:16] = pack(">H", sum(attribute_data[0:14])) + log.debug("set ndef attributes %s", hexlify(attribute_data).decode()) + self.write_to_ndef_service(attribute_data, 0) + + # If required, we will also overwrite the memory with the + # 8-bit integer provided. This could take a while. + if wipe is not None: + data = bytearray([wipe]) * 16 + while nmaxb > 0: + self.write_to_ndef_service(data, nmaxb) + nmaxb = nmaxb - 1 + + return True + + def polling(self, system_code=0xffff, request_code=0, time_slots=0): + """Aquire and identify a card. + + The Polling command is used to detect the Type 3 Tags in the + field. It is also used for initialization and anti-collision. + + The *system_code* identifies the card system to acquire. A + card can have multiple systems. The first system that matches + *system_code* will be activated. A value of 0xff for any of + the two bytes works as a wildcard, thus 0xffff activates the + very first system in the card. The card identification data + returned are the Manufacture ID (IDm) and Manufacture + Parameter (PMm). + + The *request_code* tells the card whether it should return + additional information. The default value 0 requests no + additional information. Request code 1 means that the card + shall also return the system code, so polling for system code + 0xffff with request code 1 can be used to identify the first + system on the card. Request code 2 asks for communication + performance data, more precisely a bitmap of possible + communication speeds. Not all cards provide that information. + + The number of *time_slots* determines whether there's a chance + to receive a response if multiple Type 3 Tags are in the + field. For the reader the number of time slots determines the + amount of time to wait for a response. Any Type 3 Tag in the + field, i.e. powered by the field, will choose a random time + slot to respond. With the default *time_slots* value 0 there + will only be one time slot available for all responses and + multiple responses would produce a collision. More time slots + reduce the chance of collisions (but may result in an + application working with a tag that was just accidentially + close enough). Only specific values should be used for + *time_slots*, those are 0, 1, 3, 7, and 15. Other values may + produce unexpected results depending on the tag product. + + :meth:`polling` returns either the tuple (IDm, PMm) or the + tuple (IDm, PMm, *additional information*) depending on the + response lengt, all as bytearrays. + + Command execution errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + + log.debug("polling for system 0x{0:04x}".format(system_code)) + if time_slots not in (0, 1, 3, 7, 15): + log.debug("invalid number of time slots: {0}".format(time_slots)) + raise ValueError("invalid number of time slots") + if request_code not in (0, 1, 2): + log.debug("invalid request code value: {0}".format(request_code)) + raise ValueError("invalid request code for polling") + + timeout = 0.003625 + time_slots * 0.001208 + data = pack(">HBB", system_code, request_code, time_slots) + data = self.send_cmd_recv_rsp(0x00, data, timeout, send_idm=False) + if len(data) != (16 if request_code == 0 else 18): + log.debug("unexpected polling response length") + raise Type3TagCommandError(DATA_SIZE_ERROR) + + return (data[0:8], data[8:16]) if len(data) == 16 else \ + (data[0:8], data[8:16], data[16:18]) + + def read_without_encryption(self, service_list, block_list): + """Read data blocks from unencrypted services. + + This method sends a Read Without Encryption command to the + tag. The data blocks to read are indicated by a sequence of + :class:`~nfc.tag.tt3.BlockCode` objects in *block_list*. Each + block code must reference a :class:`~nfc.tag.tt3.ServiceCode` + object from the iterable *service_list*. If any of the blocks + and services do not exist, the tag will stop processing at + that point and return a two byte error status. The status + bytes become the :attr:`~nfc.tag.TagCommandError.errno` value + of the :exc:`~nfc.tag.TagCommandError` exception. + + As an example, the following code reads block 5 from service + 16 (service type 'random read-write w/o key') and blocks 0 to + 1 from service 80 (service type 'random read-only w/o key'):: + + sc1 = nfc.tag.tt3.ServiceCode(16, 0x09) + sc2 = nfc.tag.tt3.ServiceCode(80, 0x0B) + bc1 = nfc.tag.tt3.BlockCode(5, service=0) + bc2 = nfc.tag.tt3.BlockCode(0, service=1) + bc3 = nfc.tag.tt3.BlockCode(1, service=1) + try: + data = tag.read_without_encryption([sc1, sc2], [bc1, bc2, bc3]) + except nfc.tag.TagCommandError as e: + if e.errno > 0x00FF: + print("the tag returned an error status") + else: + print("command failed with some other error") + + Command execution errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + a, b, e = self.pmm[5] & 7, self.pmm[5] >> 3 & 7, self.pmm[5] >> 6 + timeout = 302.1E-6 * ((b + 1) * len(block_list) + a + 1) * 4**e + + data = bytearray([ + len(service_list)]) \ + + b''.join([sc.pack() for sc in service_list]) \ + + bytearray([len(block_list)]) \ + + b''.join([bc.pack() for bc in block_list]) + + log.debug("read w/o encryption service/block list: {0} / {1}".format( + ' '.join([hexlify(sc.pack()).decode() for sc in service_list]), + ' '.join([hexlify(bc.pack()).decode() for bc in block_list]))) + + data = self.send_cmd_recv_rsp(0x06, data, timeout) + + if len(data) != 1 + len(block_list) * 16: + log.debug("insufficient data received from tag") + raise Type3TagCommandError(DATA_SIZE_ERROR) + + return data[1:] + + def read_from_ndef_service(self, *blocks): + """Read block data from an NDEF compatible tag. + + This is a convinience method to read block data from a tag + that has system code 0x12FC (NDEF). For other tags this method + simply returns :const:`None`. All arguments are block numbers + to read. To actually pass a list of block numbers requires + unpacking. The following example calls would have the same + effect of reading 32 byte data from from blocks 1 and 8.:: + + data = tag.read_from_ndef_service(1, 8) + data = tag.read_from_ndef_service(*list(1, 8)) + + Command execution errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + if self.sys == 0x12FC: + sc_list = [ServiceCode(0, 0b001011)] + bc_list = [BlockCode(n) for n in blocks] + return self.read_without_encryption(sc_list, bc_list) + + def write_without_encryption(self, service_list, block_list, data): + """Write data blocks to unencrypted services. + + This method sends a Write Without Encryption command to the + tag. The data blocks to overwrite are indicated by a sequence + of :class:`~nfc.tag.tt3.BlockCode` objects in the parameter + *block_list*. Each block code must reference one of the + :class:`~nfc.tag.tt3.ServiceCode` objects in the iterable + *service_list*. If any of the blocks or services do not exist, + the tag will stop processing at that point and return a two + byte error status. The status bytes become the + :attr:`~nfc.tag.TagCommandError.errno` value of the + :exc:`~nfc.tag.TagCommandError` exception. The *data* to write + must be a byte string or array of length ``16 * + len(block_list)``. + + As an example, the following code writes ``16 * "\\xAA"`` to + block 5 of service 16, ``16 * "\\xBB"`` to block 0 of service + 80 and ``16 * "\\xCC"`` to block 1 of service 80 (all services + are writeable without key):: + + sc1 = nfc.tag.tt3.ServiceCode(16, 0x09) + sc2 = nfc.tag.tt3.ServiceCode(80, 0x09) + bc1 = nfc.tag.tt3.BlockCode(5, service=0) + bc2 = nfc.tag.tt3.BlockCode(0, service=1) + bc3 = nfc.tag.tt3.BlockCode(1, service=1) + sc_list = [sc1, sc2] + bc_list = [bc1, bc2, bc3] + data = 16 * "\\xAA" + 16 * "\\xBB" + 16 * "\\xCC" + try: + data = tag.write_without_encryption(sc_list, bc_list, data) + except nfc.tag.TagCommandError as e: + if e.errno > 0x00FF: + print("the tag returned an error status") + else: + print("command failed with some other error") + + Command execution errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + a, b, e = self.pmm[6] & 7, self.pmm[6] >> 3 & 7, self.pmm[6] >> 6 + timeout = 302.1E-6 * ((b + 1) * len(block_list) + a + 1) * 4**e + + data = bytearray([ + len(service_list)]) \ + + b"".join([sc.pack() for sc in service_list]) \ + + bytearray([len(block_list)]) \ + + b"".join([bc.pack() for bc in block_list]) \ + + bytearray(data) + + log.debug("write w/o encryption service/block list: {0} / {1}".format( + ' '.join([hexlify(sc.pack()).decode() for sc in service_list]), + ' '.join([hexlify(bc.pack()).decode() for bc in block_list]))) + + self.send_cmd_recv_rsp(0x08, data, timeout) + + def write_to_ndef_service(self, data, *blocks): + """Write block data to an NDEF compatible tag. + + This is a convinience method to write block data to a tag that + has system code 0x12FC (NDEF). For other tags this method + simply does nothing. The *data* to write must be a string or + bytearray with length equal ``16 * len(blocks)``. All + parameters following *data* are interpreted as block numbers + to write. To actually pass a list of block numbers requires + unpacking. The following example calls would have the same + effect of writing 32 byte zeros into blocks 1 and 8.:: + + tag.write_to_ndef_service(32 * "\\0", 1, 8) + tag.write_to_ndef_service(32 * "\\0", *list(1, 8)) + + Command execution errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + if self.sys == 0x12FC: + sc_list = [ServiceCode(0, 0b001001)] + bc_list = [BlockCode(n) for n in blocks] + self.write_without_encryption(sc_list, bc_list, data) + + def send_cmd_recv_rsp(self, cmd_code, cmd_data, timeout, + send_idm=True, check_status=True): + """Send a command and receive a response. + + This low level method sends an arbitrary command with the + 8-bit integer *cmd_code*, followed by the captured tag + identifier (IDm) if *send_idm* is :const:`True` and the byte + string or bytearray *cmd_data*. It then waits *timeout* + seconds for a response, verifies that the response is + correctly formatted and, if *check_status* is :const:`True`, + that the status flags do not indicate an error. + + All errors raise a :exc:`~nfc.tag.TagCommandError` + exception. Errors from response status flags produce an + :attr:`~nfc.tag.TagCommandError.errno` that is greater than + 255, all other errors are below 256. + + """ + idm = self.idm if send_idm else bytearray() + cmd = bytearray([2+len(idm)+len(cmd_data), cmd_code]) + idm + cmd_data + log.debug(">> {0:02x} {1:02x} {2} {3} ({4}s)".format( + cmd[0], cmd[1], hexlify(cmd[2:10]).decode(), + hexlify(cmd[10:]).decode(), timeout)) + + started = time.time() + error = None + for retry in range(3): + try: + rsp = self.clf.exchange(cmd, timeout) + break + except nfc.clf.CommunicationError as e: + error = e + reason = error.__class__.__name__ + log.debug("%s after %d retries" % (reason, retry)) + else: + if type(error) is nfc.clf.TimeoutError: + raise Type3TagCommandError(nfc.tag.TIMEOUT_ERROR) + if type(error) is nfc.clf.TransmissionError: + raise Type3TagCommandError(nfc.tag.RECEIVE_ERROR) + if type(error) is nfc.clf.ProtocolError: # pragma: no branch + raise Type3TagCommandError(nfc.tag.PROTOCOL_ERROR) + + if rsp[0] != len(rsp): + log.debug("incorrect response length {0:02x}".format(rsp[0])) + raise Type3TagCommandError(RSP_LENGTH_ERROR) + if rsp[1] != cmd_code + 1: + log.debug("incorrect response code {0:02x}".format(rsp[1])) + raise Type3TagCommandError(RSP_CODE_ERROR) + if send_idm and rsp[2:10] != self.idm: + log.debug("wrong tag or transaction id {}".format( + hexlify(rsp[2:10]).decode())) + raise Type3TagCommandError(TAG_IDM_ERROR) + if not send_idm: + log.debug("<< {0:02x} {1:02x} {2}".format( + rsp[0], rsp[1], hexlify(rsp[2:]).decode())) + return rsp[2:] + if check_status and rsp[10] != 0: + log.debug("tag returned error status {}".format( + hexlify(rsp[10:12]).decode())) + raise Type3TagCommandError(unpack(">H", rsp[10:12])[0]) + if not check_status: + log.debug("<< {0:02x} {1:02x} {2} {3}".format( + rsp[0], rsp[1], hexlify(rsp[2:10]).decode(), + hexlify(rsp[10:]).decode())) + return rsp[10:] + log.debug("<< {0:02x} {1:02x} {2} {3} {4} ({elapsed:f}s)".format( + rsp[0], rsp[1], hexlify(rsp[2:10]).decode(), + hexlify(rsp[10:12]).decode(), hexlify(rsp[12:]).decode(), + elapsed=time.time()-started)) + return rsp[12:] + + +class Type3TagEmulation(nfc.tag.TagEmulation): + """Framework for Type 3 Tag emulation. + + """ + def __init__(self, clf, target): + self.services = dict() + self.target = target + self.cmd = bytearray([len(target.tt3_cmd)+1]) + target.tt3_cmd + self.idm = target.sensf_res[1:9] + self.pmm = target.sensf_res[9:17] + self.sys = target.sensf_res[17:19] + self.clf = clf + + def __str__(self): + """x.__str__() <==> str(x)""" + return "Type3TagEmulation IDm={id} PMm={pmm} SYS={sys}".format( + id=hexlify(self.idm).decode(), + pmm=hexlify(self.pmm).decode(), + sys=hexlify(self.sys).decode()) + + def add_service(self, service_code, block_read_func, block_write_func): + def default_block_read(block_number, rb, re): + return None + + def default_block_write(block_number, block_data, wb, we): + return False + + if block_read_func is None: + block_read_func = default_block_read + + if block_write_func is None: + block_write_func = default_block_write + + self.services[service_code] = (block_read_func, block_write_func) + + def process_command(self, cmd): + log.debug("cmd: %s", hexlify(cmd).decode() if cmd else str(cmd)) + if len(cmd) != cmd[0]: + log.error("tt3 command length error") + return None + if tuple(cmd[0:4]) in [(6, 0, 255, 255), (6, 0) + tuple(self.sys)]: + log.debug("process 'polling' command") + rsp = self.polling(cmd[2:]) + return bytearray([2 + len(rsp), 0x01]) + rsp + if cmd[2:10] == self.idm: + if cmd[1] == 0x04: + log.debug("process 'request response' command") + rsp = self.request_response(cmd[10:]) + return bytearray([10 + len(rsp), 0x05]) + self.idm + rsp + if cmd[1] == 0x06: + log.debug("process 'read without encryption' command") + rsp = self.read_without_encryption(cmd[10:]) + return bytearray([10 + len(rsp), 0x07]) + self.idm + rsp + if cmd[1] == 0x08: + log.debug("process 'write without encryption' command") + rsp = self.write_without_encryption(cmd[10:]) + return bytearray([10 + len(rsp), 0x09]) + self.idm + rsp + if cmd[1] == 0x0C: + log.debug("process 'request system code' command") + rsp = self.request_system_code(cmd[10:]) + return bytearray([10 + len(rsp), 0x0D]) + self.idm + rsp + + def send_response(self, rsp, timeout): + log.debug("rsp: {}".format(hexlify(rsp).decode() + if rsp is not None + else 'None')) + return self.clf.exchange(rsp, timeout) + + def polling(self, cmd_data): + if cmd_data[2] == 1: + rsp = self.idm + self.pmm + self.sys + else: + rsp = self.idm + self.pmm + return rsp + + def request_response(self, cmd_data): + return bytearray([0]) + + def read_without_encryption(self, cmd_data): + service_list = cmd_data.pop(0) * [[None, None]] + for i in range(len(service_list)): + service_code = cmd_data[1] << 8 | cmd_data[0] + if service_code not in self.services.keys(): + return bytearray([0xFF, 0xA1]) + service_list[i] = [service_code, 0] + del cmd_data[0:2] + + service_block_list = cmd_data.pop(0) * [None] + if len(service_block_list) > 15: + return bytearray([0xFF, 0xA2]) + for i in range(len(service_block_list)): + try: + service_list_item = service_list[cmd_data[0] & 0x0F] + service_code = service_list_item[0] + service_list_item[1] += 1 + except IndexError: + return bytearray([1 << (i % 8), 0xA3]) + if cmd_data[0] >= 128: + block_number = cmd_data[1] + del cmd_data[0:2] + else: + block_number = cmd_data[2] << 8 | cmd_data[1] + del cmd_data[0:3] + service_block_list[i] = [service_code, block_number, 0] + + service_block_count = dict(service_list) + for service_block_list_item in service_block_list: + service_code = service_block_list_item[0] + service_block_list_item[2] = service_block_count[service_code] + + block_data = bytearray() + for i, service_block_list_item in enumerate(service_block_list): + service_code, block_number, block_count = service_block_list_item + # rb (read begin) and re (read end) mark an atomic read + rb = bool(block_count == service_block_count[service_code]) + service_block_count[service_code] -= 1 + re = bool(service_block_count[service_code] == 0) + read_func, write_func = self.services[service_code] + one_block_data = read_func(block_number, rb, re) + if one_block_data is None: + return bytearray([1 << (i % 8), 0xA2]) + block_data.extend(one_block_data) + + return bytearray([0, 0, int(math.floor(len(block_data)/16))]) \ + + block_data + + def write_without_encryption(self, cmd_data): + service_list = cmd_data.pop(0) * [[None, None]] + for i in range(len(service_list)): + service_code = cmd_data[1] << 8 | cmd_data[0] + if service_code not in self.services.keys(): + return bytearray([255, 0xA1]) + service_list[i] = [service_code, 0] + del cmd_data[0:2] + + service_block_list = cmd_data.pop(0) * [None] + for i in range(len(service_block_list)): + try: + service_list_item = service_list[cmd_data[0] & 0x0F] + service_code = service_list_item[0] + service_list_item[1] += 1 + except IndexError: + return bytearray([1 << (i % 8), 0xA3]) + if cmd_data[0] >= 128: + block_number = cmd_data[1] + del cmd_data[0:2] + else: + block_number = cmd_data[2] << 8 | cmd_data[1] + del cmd_data[0:3] + service_block_list[i] = [service_code, block_number, 0] + + service_block_count = dict(service_list) + for service_block_list_item in service_block_list: + service_code = service_block_list_item[0] + service_block_list_item[2] = service_block_count[service_code] + + block_data = cmd_data[0:] + if len(block_data) % 16 != 0: + return bytearray([255, 0xA2]) + + for i, service_block_list_item in enumerate(service_block_list): + service_code, block_number, block_count = service_block_list_item + # wb (write begin) and we (write end) mark an atomic write + wb = bool(block_count == service_block_count[service_code]) + service_block_count[service_code] -= 1 + we = bool(service_block_count[service_code] == 0) + read_func, write_func = self.services[service_code] + if not write_func(block_number, block_data[i*16:(i+1)*16], wb, we): + return bytearray([1 << (i % 8), 0xA2]) + + return bytearray([0, 0]) + + def request_system_code(self, cmd_data): + return b'\x01' + self.sys + + +def activate(clf, target): + if not target.sensf_res[1:3] == b"\x01\xFE": + import nfc.tag.tt3_sony + tag = nfc.tag.tt3_sony.activate(clf, target) + return tag if tag else Type3Tag(clf, target) diff --git a/src/lib/nfc/tag/tt3_sony.py b/src/lib/nfc/tag/tt3_sony.py new file mode 100644 index 0000000..9bab877 --- /dev/null +++ b/src/lib/nfc/tag/tt3_sony.py @@ -0,0 +1,987 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2014, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import nfc.tag +from . import tt3 + +import os +import struct +from binascii import hexlify +from pyDes import triple_des, CBC +from struct import pack, unpack +import itertools + +import logging +log = logging.getLogger(__name__) + + +def activate(clf, target): + # http://www.sony.net/Products/felica/business/tech-support/list.html + ic_code = target.sensf_res[10] + if ic_code in FelicaLite.IC_CODE_MAP.keys(): + return FelicaLite(clf, target) + if ic_code in FelicaLiteS.IC_CODE_MAP.keys(): + return FelicaLiteS(clf, target) + if ic_code in FelicaStandard.IC_CODE_MAP.keys(): + return FelicaStandard(clf, target) + if ic_code in FelicaMobile.IC_CODE_MAP.keys(): + return FelicaMobile(clf, target) + if ic_code in FelicaPlug.IC_CODE_MAP.keys(): + return FelicaPlug(clf, target) + return None + + +class FelicaStandard(tt3.Type3Tag): + """Standard FeliCa is a range of FeliCa OS based card products with a + flexible file system that supports multiple applications and + services on the same card. Services can individually be protected + with a card key and all communication with protected services is + encrypted. + + """ + IC_CODE_MAP = { + # IC IC-NAME NBR NBW + 0x00: ("RC-S830", 8, 8), # RC-S831/833 + 0x01: ("RC-S915", 12, 8), # RC-S860/862/863/864/891 + 0x02: ("RC-S919", 1, 1), # RC-S890 + 0x08: ("RC-S952", 12, 8), + 0x09: ("RC-S953", 12, 8), + 0x0B: ("RC-S???", 1, 1), # new suica + 0x0C: ("RC-S954", 12, 8), + 0x0D: ("RC-S960", 12, 10), # RC-S880/889 + 0x20: ("RC-S962", 12, 10), # RC-S885/888/892/893 + 0x32: ("RC-SA00/1", 1, 1), # AES chip + 0x35: ("RC-SA00/2", 1, 1), + } + + def __init__(self, clf, target): + super(FelicaStandard, self).__init__(clf, target) + self._product = "FeliCa Standard ({0})".format( + self.IC_CODE_MAP[self.pmm[1]][0]) + + def _is_present(self): + # Perform a presence check. Modern FeliCa cards implement the + # RequestResponse command, so we'll try that first. If it + # fails we resort the generic way that works for all type 3 + # tags (but resets the card operating mode to zero). + try: + return self.request_response() in (0, 1, 2, 3) + except tt3.Type3TagCommandError: + return super(FelicaStandard, self)._is_present() + + def dump(self): + # Dump the content of a FeliCa card as good as possible. This + # is unfortunately rather complex because we want to reflect + # the area structure with indentation and summarize overlapped + # services under a single item. + + def print_system(system_code): + # Print system information + system_code_map = { + 0x0000: "SDK Sample", + 0x0003: "Suica", + 0x12FC: "NDEF", + 0x811D: "Edy", + 0x8620: "Blackboard", + 0xFE00: "Common Area", + } + return ["System {0:04X} ({1})".format( + system_code, system_code_map.get(system_code, 'unknown'))] + + def print_area(area_from, area_last, depth): + # Prints area information with indentation. + return ["{indent}Area {0:04X}--{1:04X}".format( + area_from, area_last, indent=depth*' ')] + + def print_service(services, depth): + # This function processes a list of overlapped services + # and reads all block data if there is one service that + # does not require a key. First we figure out the common + # service type and which access modes are available. + if services[0] >> 2 & 0b1111 == 0b0010: + service_type = "Random" + access_types = " & ".join([( + "write with key", "write w/o key", + "read with key", "read w/o key")[x & 3] for x in services]) + if services[0] >> 2 & 0b1111 == 0b0011: + service_type = "Cyclic" + access_types = " & ".join([( + "write with key", "write w/o key", + "read with key", "read w/o key")[x & 3] for x in services]) + if services[0] >> 2 & 0b1110 == 0b0100: + service_type = "Purse" + access_types = " & ".join([( + "direct with key", "direct w/o key", + "cashback with key", "cashback w/o key", + "decrement with key", "decrement w/o key", + "read with key", "read w/o key")[x & 7] for x in services]) + # Now we print one line to verbosely describe the service + # and list the service codes. + service_codes = " ".join(["0x{0:04X}".format(x) for x in services]) + lines = [ + "{indent}{type} Service {number}: {access} ({0})".format( + service_codes, indent=depth*' ', type=service_type, + number=services[0] >> 6, access=access_types)] + # The final piece is to see if any of the services allows + # us to read block data without a key. Services w/o key + # have the last bit set to 1, so we generate a list of + # only those services and iterate over the slice from the + # last item to end (that's one or zero services). + for service in [sc for sc in services if sc & 1][-1:]: + sc = tt3.ServiceCode(service >> 6, service & 0b111111) + for line in self.dump_service(sc): + lines.append(depth*' ' + ' ' + line) + return lines + + # Unfortunately there are some older cards with reduced + # command support. If request_system_code() is not supported + # we can only see if the current system code is NDEF and try + # to dup that, otherwise it is the end. + try: + card_system_codes = self.request_system_code() + except nfc.tag.TagCommandError: + if self.sys == 0x12FC: + return super(FelicaStandard, self).dump() + else: + return ["unable to create a memory dump"] + + # A FeliCa card has one or more systems, each system has one + # or more areas which may be nested, and an area may have zero + # to many services. The outer loop iterates over all system + # codes that are present on the card. The inner loop iterates + # by index over all area and service definitions. + lines = [] + for system_code in card_system_codes: + + # A system must be activated first, this is what the + # polling() command does. + idm, pmm = self.polling(system_code) + self.idm = idm + self.pmm = pmm + self.sys = system_code + lines.extend(print_system(system_code)) + + area_stack = [] + overlap_services = [] + + # Walk through the list of services by index. The first + # index for which there is no service returns None and + # terminate the loop. + for service_index in itertools.count(): # pragma: no branch + assert service_index < 0x10000 + depth = len(area_stack) + area_or_service = self.search_service_code(service_index) + if area_or_service is None: + # Went beyond the service index. Print overlap + # services if any and exit loop. + if len(overlap_services) > 0: + lines.extend(print_service(overlap_services, depth)) + overlap_services = [] + break + elif len(area_or_service) == 1: + # Found a service definition. Add as overlap + # service if it is either the first or same type + # (Random, Cyclic, Purse) as the previous one. If + # it is different then print the current overlap + # services and remember this for the next round. + service = area_or_service[0] + end_overlap_services = False + if len(overlap_services) == 0: + overlap_services.append(service) + elif service >> 4 == overlap_services[-1] >> 4: + if service >> 4 & 1: # purse + overlap_services.append(service) + elif service >> 2 == overlap_services[-1] >> 2: + overlap_services.append(service) + else: + end_overlap_services = True + else: + end_overlap_services = True + if end_overlap_services: + lines.extend(print_service(overlap_services, depth)) + overlap_services = [service] + elif len(area_or_service) == 2: + # Found an area definition. Print any services + # that we might so far have assembled, then + # process the area information. + if len(overlap_services) > 0: + lines.extend(print_service(overlap_services, depth)) + overlap_services = [] + area_from, area_last = area_or_service + if len(area_stack) > 0 and area_from > area_stack[-1][1]: + area_stack.pop() + lines.extend(print_area(area_from, area_last, depth)) + area_stack.append((area_from, area_last)) + + return lines + + def request_service(self, service_list): + """Verify existence of a service (or area) and get the key version. + + Each service (or area) to verify must be given as a + :class:`~nfc.tag.tt3.ServiceCode` in the iterable + *service_list*. The key versions are returned as a list of + 16-bit integers, in the order requested. If a specified + service (or area) does not exist, the key version will be + 0xFFFF. + + Command execution errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + a, b, e = self.pmm[2] & 7, self.pmm[2] >> 3 & 7, self.pmm[2] >> 6 + timeout = 302E-6 * ((b + 1) * len(service_list) + a + 1) * 4**e + pack = lambda x: x.pack() # noqa: E731 + data = bytearray([len(service_list)]) \ + + b''.join(map(pack, service_list)) + data = self.send_cmd_recv_rsp(0x02, data, timeout, check_status=False) + if len(data) != 1 + len(service_list) * 2: + log.debug("insufficient data received from tag") + raise tt3.Type3TagCommandError(tt3.DATA_SIZE_ERROR) + return [unpack("> 3 & 7, self.pmm[3] >> 6 + timeout = 302E-6 * (b + 1 + a + 1) * 4**e + data = self.send_cmd_recv_rsp(0x04, b'', timeout, check_status=False) + if len(data) != 1: + log.debug("insufficient data received from tag") + raise tt3.Type3TagCommandError(tt3.DATA_SIZE_ERROR) + return data[0] # mode + + def search_service_code(self, service_index): + """Search for a service code that corresponds to an index. + + The Search Service Code command provides access to the + iterable list of services and areas within the activated + system. The *service_index* argument may be any value from 0 + to 0xffff. As long as there is a service or area found for a + given *service_index*, the information returned is a tuple + with either one or two 16-bit integer elements. Two integers + are returned for an area definition, the first is the area + code and the second is the largest possible service index for + the area. One integer, the service code, is returned for a + service definition. The return value is :const:`None` if the + *service_index* was not found. + + For example, to print all services and areas of the active + system: :: + + for i in xrange(0x10000): + area_or_service = tag.search_service_code(i) + if area_or_service is None: + break + elif len(area_or_service) == 1: + sc = area_or_service[0] + print(nfc.tag.tt3.ServiceCode(sc >> 6, sc & 0x3f)) + elif len(area_or_service) == 2: + area_code, area_last = area_or_service + print("Area {0:04x}--{0:04x}".format(area_code, area_last)) + + Command execution errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + log.debug("search service code index {0}".format(service_index)) + # The maximum response time is given by the value of PMM[3]. + # Some cards (like RC-S860 with IC RC-S915) encode a value + # that is too short, thus we use at lest 2 ms. + a, e = self.pmm[3] & 7, self.pmm[3] >> 6 + timeout = max(302E-6 * (a + 1) * 4**e, 0.002) + data = pack("> 6 + timeout = max(302E-6 * (a + 1) * 4**e, 0.002) + data = self.send_cmd_recv_rsp(0x0C, b'', timeout, check_status=False) + if len(data) != 1 + data[0] * 2: + log.debug("insufficient data received from tag") + raise tt3.Type3TagCommandError(tt3.DATA_SIZE_ERROR) + return [unpack(">H", data[i:i+2])[0] for i in range(1, len(data), 2)] + + +class FelicaMobile(FelicaStandard): + """Mobile FeliCa is a modification of FeliCa for use in mobile + phones. This class does currently not implement anything specific + beyond recognition of the Mobile FeliCa OS version. + + """ + IC_CODE_MAP = { + # IC IC-NAME NBR NBW + 0x06: ("1.0", 1, 1), + 0x07: ("1.0", 1, 1), + 0x10: ("2.0", 1, 1), + 0x11: ("2.0", 1, 1), + 0x12: ("2.0", 1, 1), + 0x13: ("2.0", 1, 1), + 0x14: ("3.0", 1, 1), + 0x15: ("3.0", 1, 1), + 0x16: ("3.0", 1, 1), + 0x17: ("3.0", 1, 1), + 0x18: ("3.0", 1, 1), + 0x19: ("3.0", 1, 1), + 0x1A: ("3.0", 1, 1), + 0x1B: ("3.0", 1, 1), + 0x1C: ("3.0", 1, 1), + 0x1D: ("3.0", 1, 1), + 0x1E: ("3.0", 1, 1), + 0x1F: ("3.0", 1, 1), + } + + def __init__(self, clf, target): + super(FelicaMobile, self).__init__(clf, target) + self._product = "FeliCa Mobile " + self.IC_CODE_MAP[self.pmm[1]][0] + + +class FelicaLite(tt3.Type3Tag): + """FeliCa Lite is a version of FeliCa with simplified file system and + security functions. The usable memory is 13 blocks (one block has + 16 byte) plus a one block subtraction register. The tag can be + configured with a card key to authenticate the tag and protect + integrity of data reads. + + """ + IC_CODE_MAP = { + 0xF0: "FeliCa Lite (RC-S965)", + } + + class NDEF(tt3.Type3Tag.NDEF): + def _read_attribute_data(self): + log.debug("FelicaLite.read_attribute_data") + attributes = super(FelicaLite.NDEF, self)._read_attribute_data() + if attributes is not None and self._tag.is_authenticated: + # when authenticated we need to make room for the mac + self._original_nbr = attributes['nbr'] + attributes['nbr'] = min(attributes['nbr'], 3) + return attributes + + def _write_attribute_data(self, attributes): + log.debug("FelicaLite.read_attribute_data") + if self._tag.is_authenticated: + attributes = attributes.copy() + attributes['nbr'] = self._original_nbr + super(FelicaLite.NDEF, self)._write_attribute_data(attributes) + + def __init__(self, clf, target): + super(FelicaLite, self).__init__(clf, target) + self._product = self.IC_CODE_MAP[self.pmm[1]] + self._sk = self._iv = None + self.read_from_ndef_service = self.read_without_mac + self.write_to_ndef_service = self.write_without_mac + + def dump(self): + def oprint(octets): + return ' '.join(['%02x' % x for x in octets]) + + def cprint(octets): + return ''.join([chr(x) if 32 <= x <= 126 else '.' for x in octets]) + + userblocks = list() + for i in range(0, 14): + try: + data = self.read_without_mac(i) + except tt3.Type3TagCommandError: + userblocks.append("{0} |{1}|".format( + " ".join(16 * ["??"]), 16*".")) + else: + userblocks.append("{0} |{1}|".format( + oprint(data), cprint(data))) + + lines = list() + last_block = None + same_blocks = 0 + + for i, block in enumerate(userblocks): + if block == last_block: + same_blocks += 1 + continue + if same_blocks: + if same_blocks > 1: + lines.append(" * " + last_block) + same_blocks = 0 + lines.append("{0:3}: ".format(i) + block) + last_block = block + + if same_blocks: + if same_blocks > 1: + lines.append(" * " + last_block) + lines.append("{0:3}: ".format(i) + block) + + data = self.read_without_mac(14) + lines.append(" 14: {0} ({1})".format(oprint(data), "REGA[4]B[4]C[8]")) + + text = ("RC1[8], RC2[8]", "MAC[8]", "IDD[8], DFC[2]", + "IDM[8], PMM[8]", "SERVICE_CODE[2]", + "SYSTEM_CODE[2]", "CKV[2]", "CK1[8], CK2[8]", + "MEMORY_CONFIG") + config = dict(zip(range(0x80, 0x80+len(text)), text)) + + for i in sorted(config.keys()): + try: + data = self.read_without_mac(i) + except tt3.Type3TagCommandError: + lines.append("{0:3}: {1}({2})".format( + i, 16 * "?? ", config[i])) + else: + lines.append("{0:3}: {1} ({2})".format( + i, oprint(data), config[i])) + + return lines + + @staticmethod + def generate_mac(data, key, iv, flip_key=False): + # Data is first split into tuples of 8 character bytes, each + # tuple then reversed and joined, finally all joined back to + # one string that is then triple des encrypted with key and + # initialization vector iv. If flip_key is True then the key + # halfs will be exchanged (this is used to generate a mac for + # write). The resulting mac is the last 8 bytes returned in + # reversed order. + assert len(data) % 8 == 0 and len(key) == 16 and len(iv) == 8 + key = bytes(key[8:] + key[:8]) if flip_key else bytes(key) + txt = b''.join([ + struct.pack("{}B".format(len(x)), *reversed(x)) + if isinstance(x[0], int) + else b''.join(reversed(x)) + for x in zip(*[iter(bytes(data))]*8)]) + return bytearray(triple_des(key, CBC, bytes(iv)).encrypt(txt)[:-9:-1]) + + def protect(self, password=None, read_protect=False, protect_from=0): + """Protect a FeliCa Lite Tag. + + A FeliCa Lite Tag can be provisioned with a custom password + (or the default manufacturer key if the password is an empty + string or bytearray) to ensure that data retrieved by future + read operations, after authentication, is genuine. Read + protection is not supported. + + A non-empty *password* must provide at least 128 bit key + material, in other words it must be a string or bytearray of + length 16 or more. + + The memory unit for the value of *protect_from* is 16 byte, + thus with ``protect_from=2`` bytes 0 to 31 are not protected. + If *protect_from* is zero (the default value) and the Tag has + valid NDEF management data, the NDEF RW Flag is set to read + only. + + """ + return super(FelicaLite, self).protect( + password, read_protect, protect_from) + + def _protect(self, password, read_protect, protect_from): + if password and len(password) < 16: + raise ValueError("password must be at least 16 byte") + + if protect_from < 0: + raise ValueError("protect_from can not be negative") + + if read_protect: + log.info("this tag can not be made read protected") + return False + + # The memory configuration block contains access permissions + # and ndef compatibility information. + mc = self.read_without_mac(0x88) + + if password is not None: + if mc[2] != 0xFF: + log.info("system block protected, can't write key") + return False + + # if password is empty use factory key of 16 zero bytes + key = password[0:16] if password else b"\0"*16 + + log.debug("protect with key %s", hexlify(key).decode()) + self.write_without_mac(key[7::-1] + key[15:7:-1], 0x87) + + if protect_from < 14: + log.debug("write protect blocks {0}--13".format(protect_from)) + mc[0:2] = pack("H', sum(attribute_data[0:14])) + self.write_without_mac(attribute_data, 0) + + log.debug("write protect system blocks 82,83,84,86,87") + mc[2] = 0x00 # set system blocks 82,83,84,86,87 to read only + + log.debug("write memory configuration %s", hexlify(mc).decode()) + self.write_without_mac(mc, 0x88) + return True + + def authenticate(self, password): + """Authenticate a FeliCa Lite Tag. + + A FeliCa Lite Tag is authenticated by a procedure that allows + both the reader and the tag to calculate a session key from a + random challenge send by the reader and a key that is securely + stored on the tag and provided to :meth:`authenticate` as the + *password* argument. If the tag was protected with an earlier + call to :meth:`protect` then the same password should + successfully authenticate. + + After authentication the :meth:`read_with_mac` method can be + used to read data such that it can not be falsified on + transmission. + + """ + return super(FelicaLite, self).authenticate(password) + + def _authenticate(self, password): + if password and len(password) < 16: + raise ValueError("password must be at least 16 byte") + + # Perform internal authentication, i.e. ensure that the tag + # has the same card key as in password. If the password is + # empty, we'll try with the factory key. + key = b"\0" * 16 if not password else password[0:16] + + log.debug("authenticate with key {}".format(hexlify(key).decode())) + self._authenticated = False + self.read_from_ndef_service = self.read_without_mac + self.write_to_ndef_service = self.write_without_mac + + # Internal authentication starts with a random challenge (rc1 || rc2) + # that we write to the rc block. Because the tag works little endian, + # we reverse the order of rc1 and rc2 bytes when writing. + rc = os.urandom(16) + log.debug("rc1 = {}".format(hexlify(rc[:8]).decode())) + log.debug("rc2 = {}".format(hexlify(rc[8:]).decode())) + self.write_without_mac(rc[7::-1] + rc[15:7:-1], 0x80) + + # The session key becomes the triple_des encryption of the random + # challenge under the card key and with an initialization vector of + # all zero. + sk = triple_des(key, CBC, b'\00' * 8).encrypt(rc) + log.debug("sk1 = {}".format(hexlify(sk[:8]).decode())) + log.debug("sk2 = {}".format(hexlify(sk[8:]).decode())) + + # By reading the id and mac block together we get the mac that the + # tag has generated over the id block data under it's session key + # generated the same way as we did) and with rc1 as the + # initialization vector. + data = self.read_without_mac(0x82, 0x81) + + # Now we check if we calculate the same mac with our session key. + # Note that, because of endianess, data must be reversed in chunks + # of 8 bytes as does the 8 byte mac - this is all done within the + # generate_mac() function. + if data[-16:-8] == self.generate_mac(data[0:-16], sk, iv=rc[0:8]): + log.debug("tag authentication completed") + self._sk = sk + self._iv = rc[0:8] + self._authenticated = True + self.read_from_ndef_service = self.read_with_mac + else: + log.debug("tag authentication failed") + + return self._authenticated + + def format(self, version=0x10, wipe=None): + """Format a FeliCa Lite Tag for NDEF. + + """ + return super(FelicaLite, self).format(version, wipe) + + def _format(self, version, wipe): + assert type(version) is int + assert wipe is None or type(wipe) is int + + if version and version >> 4 != 1: + log.error("type 3 tag ndef mapping major version must be 1") + return False + + # The memory configuration block contains access permissions + # and ndef compatibility information. + mc = self.read_without_mac(0x88) + + if mc[0] & 0x01 != 0x01: + log.info("the first user data block is not writeable") + return False + + if not mc[3] & 0x01: # ndef compatibility flag + if mc[2] == 0xFF: # mc block is writeable + mc[3] = mc[3] | 0x01 + self.write_without_mac(mc, 0x88) + else: + log.info("this tag can no longer be changed to ndef") + return False + + # Count the number of writeable data blocks (that is excluding + # the attribute block) from the least significant read/write + # permission bits that are consecutively set to 1. + rw_bits = unpack("> (nmaxb + 1) & 1 == 0: + break + + # Create and write the attribute data. Version number, Nbr and + # Nbw are fix and we have just determined Nmaxb. + attribute_data = bytearray(16) + attribute_data[:14] = pack(">BBBHxxxxxBxxx", version, 4, 1, nmaxb, 1) + attribute_data[14:] = pack(">H", sum(attribute_data[:14])) + log.debug("set ndef attributes %s", hexlify(attribute_data).decode()) + self.write_without_mac(attribute_data, 0) + + # Overwrite the ndef message area if a wipe is requested. + if wipe is not None: + data = bytearray(16 * [wipe]) + for block in range(1, nmaxb+1): + self.write_without_mac(data, block) + + return True + + def read_without_mac(self, *blocks): + """Read a number of data blocks without integrity check. + + This method accepts a variable number of integer arguments as + the block numbers to read. The blocks are read with service + code 0x000B (NDEF). + + Tag command errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + log.debug("read {0} block(s) without mac".format(len(blocks))) + service_list = [tt3.ServiceCode(0, 0b001011)] + block_list = [tt3.BlockCode(n) for n in blocks] + return self.read_without_encryption(service_list, block_list) + + def read_with_mac(self, *blocks): + """Read a number of data blocks with integrity check. + + This method accepts a variable number of integer arguments as + the block numbers to read. The blocks are read with service + code 0x000B (NDEF). Along with the requested block data the + tag returns a message authentication code that is verified + before data is returned. If verification fails the return + value of :meth:`read_with_mac` is None. + + A :exc:`RuntimeError` exception is raised if the tag was not + authenticated before calling this method. + + Tag command errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + log.debug("read {0} block(s) with mac".format(len(blocks))) + + if self._sk is None or self._iv is None: + raise RuntimeError("authentication required") + + service_list = [tt3.ServiceCode(0, 0b001011)] + block_list = [tt3.BlockCode(n) for n in blocks] + block_list.append(tt3.BlockCode(0x81)) + + data = self.read_without_encryption(service_list, block_list) + data, mac = data[0:-16], data[-16:-8] + if mac != self.generate_mac(data, self._sk, self._iv): + log.warning("mac verification failed") + else: + return data + + def write_without_mac(self, data, block): + """Write a data block without integrity check. + + This is the standard write method for a FeliCa Lite. The + 16-byte string or bytearray *data* is written to the numbered + *block* in service 0x0009 (NDEF write service). :: + + data = bytearray(range(16)) # 0x00, 0x01, ... 0x0F + try: tag.write_without_mac(data, 5) # write block 5 + except nfc.tag.TagCommandError: + print("something went wrong") + + Tag command errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + # Write a single data block without a mac. Write with mac is + # only supported by FeliCa Lite-S. + assert len(data) == 16 and type(block) is int + log.debug("write 1 block without mac".format()) + sc_list = [tt3.ServiceCode(0, 0b001001)] + bc_list = [tt3.BlockCode(block)] + self.write_without_encryption(sc_list, bc_list, data) + + +class FelicaLiteS(FelicaLite): + """FeliCa Lite-S is a version of FeliCa Lite with enhanced security + functions. It provides mutual authentication were both the tag and + the reader must demonstrate posession of the card key before data + writes can be made. It is also possible to require mutual + authentication for data reads. + + """ + IC_CODE_MAP = { + 0xF1: "FeliCa Lite-S (RC-S966)", + 0xF2: "FeliCa Link (RC-S730) Lite-S Mode", + } + + class NDEF(FelicaLite.NDEF): + def _read_attribute_data(self): + log.debug("FelicaLiteS.read_attribute_data") + attributes = super(FelicaLiteS.NDEF, self)._read_attribute_data() + if attributes is not None and self._tag._authenticated: + # when authenticated and user data is writeable + mc = self._tag.read_without_mac(0x88) + rw_bits = unpack("H', sum(attribute_data[0:14])) + self.write_without_mac(attribute_data, 0) + + log.debug("write protect system blocks 82,83,84,86,87") + mc[2] = 0x00 # set system blocks 82,83,84,86,87 to read only + mc[5] = 0x01 # but allow write with mac to ck and ckv block + + # Write the new memory control block. + log.debug("write memory configuration %s", hexlify(mc).decode()) + self.write_without_mac(mc, 0x88) + return True + + def authenticate(self, password): + """Mutually authenticate with a FeliCa Lite-S Tag. + + FeliCa Lite-S supports enhanced security functions, one of + them is the mutual authentication performed by this + method. The first part of mutual authentication is to + authenticate the tag with :meth:`FelicaLite.authenticate`. If + successful, the shared session key is used to generate the + integrity check value for write operation to update a specific + memory block. If that was successful then the tag is ensured + that the reader has the correct card key. + + After successful authentication the + :meth:`~FelicaLite.read_with_mac` and :meth:`write_with_mac` + methods can be used to read and write data such that it can + not be falsified on transmission. + + """ + if super(FelicaLiteS, self).authenticate(password): + # At this point we have achieved internal authentication, + # i.e we know that the tag has the same card key as in + # password. We now reset the authentication status and do + # external authentication to assure the tag that we have + # the right card key. + self._authenticated = False + self.read_from_ndef_service = self.read_without_mac + self.write_to_ndef_service = self.write_without_mac + + # To authenticate to the tag we write a 01h into the + # ext_auth byte of the state block (block 0x92). The other + # bytes of the state block can be all set to zero. + self.write_with_mac(b"\x01" + 15*b"\0", 0x92) + + # Now read the state block and check the value of the + # ext_auth to see if we are authenticated. If it's 01h + # then we are, otherwise not. + if self.read_with_mac(0x92)[0] == 0x01: + log.debug("mutual authentication completed") + self._authenticated = True + self.read_from_ndef_service = self.read_with_mac + self.write_to_ndef_service = self.write_with_mac + else: + log.debug("mutual authentication failed") + + return self._authenticated + + def write_with_mac(self, data, block): + """Write one data block with additional integrity check. + + If prior to calling this method the tag was not authenticated, + a :exc:`RuntimeError` exception is raised. + + Command execution errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + # Write a single data block protected with a mac. The card + # will only accept the write if it computed the same mac. + log.debug("write 1 block with mac") + if len(data) != 16: + raise ValueError("data must be 16 octets") + if type(block) is not int: + raise ValueError("block number must be int") + if self._sk is None or self._iv is None: + raise RuntimeError("tag must be authenticated first") + + # The write count is the first three byte of the wcnt block. + wcnt = self.read_without_mac(0x90)[0:3] + log.debug("write count is %s", hexlify(wcnt[::-1]).decode()) + + # We must generate the mac_a block to write the data. The data + # to encrypt to the mac is composed of write count and block + # numbers (8 byte) and the data we want to write. The mac for + # write must be generated with the key flipped (sk2 || sk1). + def flip(sk): + return sk[8:16] + sk[0:8] + + data = wcnt + b"\x00" + bytearray([block]) + b"\x00\x91\x00" + data + maca = self.generate_mac(data, flip(self._sk), self._iv) + wcnt+5*b"\0" + + # Now we can write the data block with our computed mac to the + # desired block and the maca block. Write without encryption + # means that the data is not encrypted with a service key. + sc_list = [tt3.ServiceCode(0, 0b001001)] + bc_list = [tt3.BlockCode(block), tt3.BlockCode(0x91)] + self.write_without_encryption(sc_list, bc_list, data[8:24] + maca) + + +class FelicaPlug(tt3.Type3Tag): + """FeliCa Plug is a contactless communication interface module for + microcontrollers. + + """ + IC_CODE_MAP = { + 0xE0: "FeliCa Plug (RC-S926)", + 0xE1: "FeliCa Link (RC-S730) Plug Mode", + } + + def __init__(self, clf, target): + super(FelicaPlug, self).__init__(clf, target) + self._product = self.IC_CODE_MAP[self.pmm[1]] diff --git a/src/lib/nfc/tag/tt4.py b/src/lib/nfc/tag/tt4.py new file mode 100644 index 0000000..08267eb --- /dev/null +++ b/src/lib/nfc/tag/tt4.py @@ -0,0 +1,579 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2012, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import itertools +from binascii import hexlify +from struct import pack, unpack + +import nfc.tag +import nfc.clf + +import logging +log = logging.getLogger(__name__) + + +ndef_aid_v1 = bytearray.fromhex("D2760000850100") +ndef_aid_v2 = bytearray.fromhex("D2760000850101") + + +class Type4TagCommandError(nfc.tag.TagCommandError): + """Type 4 Tag exception class. Beyond the generic error values from + :attr:`~nfc.tag.TagCommandError` this class covers ISO 7816-4 + response APDU error codes. + + """ + errno_str = { + # ISO/IEC 7816-4 (2005) APDU errors (SW1/SW2) + 0x6700: "wrong lenght (general error)", + 0x6900: "command not allowed (general error)", + 0x6981: "command incompatible with file structure", + 0x6982: "security status not satisfied", + 0x6A00: "wrong parameters p1/p2 (general error)", + 0x6A80: "incorrect parameters in command data field", + 0x6A81: "function not supported", + 0x6A82: "file or application not found", + 0x6A83: "record not found", + 0x6A84: "not enough memory space in the file", + 0x6A85: "command length inconsistent with TLV structure", + 0x6A86: "incorrect parameters p1/p2", + 0x6A87: "command length inconsistent with p1/p2", + 0x6A88: "referenced data or reference data not found", + 0x6A89: "file already exists", + 0x6A8A: "file name already exists", + } + + @staticmethod + def from_status(status): + return Type4TagCommandError(unpack(">H", status)[0]) + + +class IsoDepInitiator(object): + def __init__(self, clf, fsc, fwt): + self.clf = clf + self.pni = 0 + self.miu = fsc - 3 # account for 1 byte PCB and 2 byte EDC + self.fwt = fwt + self.delta_fwt = 49152 / 13.56E6 + self.n_retry_ack = min(int(1/self.fwt), 5) + self.n_retry_nak = self.n_retry_ack + + def exchange(self, command, timeout=None): + if timeout is None: + timeout = self.fwt + self.delta_fwt + + if command is None: + # presence check with R(NAK) + data = bytearray([0xB2 | self.pni]) + self.clf.exchange(data, timeout) + return + + for offset in range(0, len(command), self.miu): + more = len(command) - offset > self.miu + pfb = pack('B', (0x02, 0x12)[more] | self.pni) + data = pfb + command[offset:offset+self.miu] + + for i in itertools.count(start=1): # pragma: no branch + try: + data = self.clf.exchange(data, timeout) + if len(data) == 0: + raise nfc.clf.TransmissionError + if data[0] == 0xA2 | (~self.pni & 1): + log.debug("ISO-DEP retransmit after ack") + data = pfb + command[offset:offset+self.miu] + continue + break + except nfc.clf.TransmissionError: + if i <= self.n_retry_nak: + log.warning("ISO-DEP transmission error (#%d)" % i) + data = bytearray([0xB2 | self.pni]) + else: + log.error("ISO-DEP unrecoverable transmission error") + raise Type4TagCommandError(nfc.tag.RECEIVE_ERROR) + except nfc.clf.TimeoutError: + if i <= self.n_retry_nak: + log.warning("ISO-DEP timeout error (#%d)" % i) + data = bytearray([0xB2 | self.pni]) + else: + log.error("ISO-DEP unrecoverable timeout error") + raise Type4TagCommandError(nfc.tag.TIMEOUT_ERROR) + except nfc.clf.ProtocolError: + log.error("ISO-DEP unrecoverable protocol error") + raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR) + + while data[0] & 0b11111110 == 0b11110010: # WTX + log.debug("ISO-DEP waiting time extension") + data = self.clf.exchange(data, (data[1] & 0x3F) * self.fwt) + + if data[0] & 0x01 != self.pni: + log.warning("ISO-DEP protocol error: block number") + raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR) + + if more: + if data[0] & 0b11111110 == 0b10100010: # ACK + self.pni = (self.pni + 1) % 2 + else: + log.error("ISO-DEP protocol error: expected ack") + raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR) + else: + if data[0] & 0b11101110 == 0x02: # INF + self.pni = (self.pni + 1) % 2 + response = data[1:] + else: + log.error("ISO-DEP protocol error: expected inf") + raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR) + + while bool(data[0] & 0b00010000): + data = pack('B', 0xA2 | self.pni) # ACK + + for i in itertools.count(start=1): # pragma: no branch + try: + data = self.clf.exchange(data, timeout) + if len(data) == 0: + raise nfc.clf.TransmissionError + break + except nfc.clf.TransmissionError: + if i <= self.n_retry_ack: + log.warning("ISO-DEP transmission error (#%d)" % i) + data = bytearray([0xA2 | self.pni]) + else: + log.error("ISO-DEP unrecoverable transmission error") + raise Type4TagCommandError(nfc.tag.RECEIVE_ERROR) + except nfc.clf.TimeoutError: + if i <= self.n_retry_ack: + log.warning("ISO-DEP timeout error (#%d)" % i) + data = bytearray([0xA2 | self.pni]) + else: + log.error("ISO-DEP unrecoverable timeout error") + raise Type4TagCommandError(nfc.tag.TIMEOUT_ERROR) + except nfc.clf.ProtocolError: + log.error("ISO-DEP unrecoverable protocol error") + raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR) + + if data[0] & 0x01 != self.pni: + log.error("ISO-DEP protocol error: block number") + raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR) + + response = response + data[1:] + self.pni = (self.pni + 1) % 2 + + return response + + +class Type4Tag(nfc.tag.Tag): + """Implementation of the NFC Forum Type 4 Tag operation specification. + + The NFC Forum Type 4 Tag is based on ISO/IEC 14443 DEP protocol + for Type A and B modulation and uses ISO/IEC 7816-4 command and + response APDUs. + + """ + TYPE = "Type4Tag" + + class NDEF(nfc.tag.Tag.NDEF): + # Type 4 Tag specific implementation of the NDEF access type + # class that is returned by the Tag.ndef attribute. + + def _select_ndef_application(self): + for self._aid, mrl in ((ndef_aid_v2, 256), (ndef_aid_v1, 0)): + try: + self.tag.send_apdu(0, 0xA4, 0x04, 0x00, self._aid, mrl) + log.debug("selected %s", hexlify(self._aid).decode()) + return True + except Type4TagCommandError as error: + if error.errno <= 0: + break + + def _select_fid(self, fid): + p2 = 0x00 if self._aid == ndef_aid_v1 else 0x0C + try: + self.tag.send_apdu(0, 0xA4, 0x00, p2, fid) + log.debug("selected %s", hexlify(fid).decode()) + return True + except Type4TagCommandError: + log.debug("failed to select %s", hexlify(fid).decode()) + + def _read_binary(self, offset, size): + (p1, p2) = pack(">H", offset) + max_data = min(self._max_le, size) + log.debug("read_binary from %d to %d", offset, offset + max_data) + return self.tag.send_apdu(0, 0xB0, p1, p2, mrl=max_data) + + def _update_binary(self, offset, data): + (p1, p2) = pack(">H", offset) + max_data = min(self._max_lc, len(data)) + log.debug("update_binary from %d to %d", offset, offset + max_data) + self.tag.send_apdu(0, 0xD6, p1, p2, data[:max_data]) + return max_data + + def _discover_ndef(self): + self._max_lc = 1 + self._max_le = 15 + + log.debug("select ndef application") + if not self._select_ndef_application(): + log.debug("no ndef application file") + return False + + log.debug("select ndef capability file") + if not self._select_fid(b"\xE1\x03"): + log.warning("no ndef capability file") + return False + + log.debug("read ndef capability file") + cclen = self._read_binary(0, 2) + if not (cclen and len(cclen) == 2): + log.debug("error reading capability length") + return False + + cclen = unpack(">H", cclen)[0] + capabilities = self._read_binary(2, min(cclen-2, 15)) + + if capabilities is None or len(capabilities) < 13: + log.warning("insufficient capability data") + return False + + capabilities += (15-len(capabilities)) * b"\0" # for unpack + ver, mle, mlc, tag, val = unpack(">BHHB9p", capabilities) + log.debug("ndef mapping version %d.%d", ver >> 4, ver & 15) + log.debug("max apdu response length %d", mle) + log.debug("max apdu command length %d", mlc) + log.debug("ndef file control tlv tag %d", tag) + + if ver >> 4 not in (1, 2, 3): + log.debug("unsupported major ndef version") + return False + + if not (tag, len(val)) in ((4, 6), (6, 8)): + log.error("invalid ndef control tlv") + return False + + ndef_control_tlv_format = ">2sHBB" if tag == 4 else ">2sIBB" + ndef_file, mfs, rf, wf = unpack(ndef_control_tlv_format, val) + log.debug("ndef file identifier %s", hexlify(ndef_file).decode()) + log.debug("ndef file size limit %d", mfs) + log.debug("ndef file read flag is %d", rf) + log.debug("ndef file write flag is %d", wf) + + self._max_le = mle + self._max_lc = mlc + self._capacity = mfs - tag + 2 + self._readable = bool(rf == 0) + self._writeable = bool(wf == 0) + self._nlen_size = tag - 2 + self._ndef_file = ndef_file + + return True + + def _read_ndef_data(self): + log.debug("read ndef data") + + try: + if not (hasattr(self, "_ndef_file") or self._discover_ndef()): + log.debug("no ndef application") + return None + + log.debug("select ndef data file") + if not self._select_fid(self._ndef_file): + log.warning("ndef file select error") + return None + + log.debug("read ndef data file") + lfmt = ">I" if self._nlen_size == 4 else ">H" + nlen = self._read_binary(0, self._nlen_size) + if len(nlen) != self._nlen_size: + return None + + nlen = unpack(lfmt, nlen)[0] + log.debug("ndef data length is {0}".format(nlen)) + + data = bytearray() + while len(data) < nlen: + offset = self._nlen_size + len(data) + data += self._read_binary(offset, nlen - len(data)) + + except Type4TagCommandError: + return None + else: + return data + + def _write_ndef_data(self, data): + log.debug("write ndef data") + + lfmt = ">I" if self._nlen_size == 4 else ">H" + nlen = bytearray(pack(lfmt, len(data))) + if len(nlen) + len(data) <= self._max_lc: + data = bytearray(nlen) + data + nlen = None + else: + data = bytearray(len(nlen)) + data + + offset = 0 + while offset < len(data): + offset += self._update_binary(offset, data[offset:]) + + if nlen: + self._update_binary(0, nlen) + + return True + + def _wipe_ndef_data(self, wipe=None): + lfmt = ">I" if self._nlen_size == 4 else ">H" + nlen = bytearray(pack(lfmt, 0)) + self._update_binary(0, nlen) + offset = self._nlen_size + data = bytearray(self._capacity * [wipe % 256]) + while offset < self.capacity: + offset += self._update_binary(offset, data[offset:]) + + def _dump_ndef_data(self): + lines = [] + for offset in itertools.count(0, 16): # pragma: no branch + try: + line = self._read_binary(offset, 16) + if len(line) > 0: + lines.append(line) + if len(line) < 16: + break + except Type4TagCommandError: + break + + return lines + + def _is_present(self): + try: + self._dep.exchange(None) + return True + except nfc.clf.CommunicationError: + return False + + def dump(self): + """Returns tag data as a list of formatted strings. + + The :meth:`dump` method provides useful output only for NDEF + formatted Type 4 Tags. Each line that is returned contains a + hexdump of 16 octets from the NDEF data file. + + """ + return self._dump() + + def _dump(self): + def oprint(octets): + return ' '.join(['%02x' % x for x in octets]) + + def cprint(octets): + return ''.join([chr(x) if 32 <= x <= 126 else '.' for x in octets]) + + def lprint(fmt, octets, index): + return fmt.format(index, oprint(octets), cprint(octets)) + + lfmt = "0x{0:04x}: {1} |{2}|" + + if self.ndef and self.ndef.is_readable: + lines = self.ndef._dump_ndef_data() + return [lprint(lfmt, d, i << 4) for i, d in enumerate(lines)] + + return [] + + def format(self, version=None, wipe=None): + """Erase the NDEF message on a Type 4 Tag. + + The :meth:`format` method writes the length of the NDEF + message on a Type 4 Tag to zero, thus the tag will appear to + be empty. If the *wipe* argument is set to some integer then + :meth:`format` will also overwrite all user data with that + integer (mod 256). + + Despite it's name, the :meth:`format` method can not format a + blank tag to make it NDEF compatible; this requires + proprietary information from the manufacturer. + + """ + return super(Type4Tag, self).format(version, wipe) + + def _format(self, version, wipe): + if not self.ndef or not self.ndef.is_writeable: + log.error("format error: no ndef or not writeable") + return False + + if wipe is not None: + try: + self.ndef._wipe_ndef_data(wipe) + except Type4TagCommandError as error: + log.error("format error: %s", str(error)) + return False + + return True + + def transceive(self, data, timeout=None): + """Transmit arbitrary data and receive the response. + + This is a low level method to send arbitrary data to the + tag. While it should almost always be better to use + :meth:`send_apdu` this is the only way to force a specific + timeout value (which is otherwise derived from the Tag's + answer to select). The *timeout* value is expected as a float + specifying the seconds to wait. + + """ + log.debug(">> {0}".format(hexlify(data).decode())) + data = self._dep.exchange(data, timeout) + log.debug("<< {0}".format(hexlify(data).decode() if data else "None")) + return data + + def send_apdu(self, cla, ins, p1, p2, data=None, mrl=0, check_status=True): + """Send an ISO/IEC 7816-4 APDU to the Type 4 Tag. + + The 4 byte APDU header (class, instruction, parameter 1 and 2) + is constructed from the first four parameters (cla, ins, p1, + p2) without interpretation. The byte string *data* argument + represents the APDU command data field. It is encoded as a + short or extended length field followed by the *data* + bytes. The length field is not transmitted if *data* is None + or an empty string. The maximum acceptable number of response + data bytes is given with the max-response-length *mrl* + argument. The value of *mrl* is transmitted as the 7816-4 APDU + Le field after appropriate conversion. + + By default, the response is returned as a byte array not + including the status word, a :exc:`Type4TagCommandError` + exception is raised for any status word other than + 9000h. Response status verification can be disabled with + *check_status* set to False, the byte array will then include + the response status word at the last two positions. + + Transmission errors always raise a :exc:`Type4TagCommandError` + exception. + + """ + apdu = bytearray([cla, ins, p1, p2]) + + if not self._extended_length_support: + if data and len(data) > 255: + raise ValueError("unsupported command data length") + if mrl and mrl > 256: + raise ValueError("unsupported max response length") + if data: + apdu += pack('>B', len(data)) + bytes(data) + if mrl > 0: + apdu += pack('>B', 0 if mrl == 256 else mrl) + else: + if data and len(data) > 65535: + raise ValueError("invalid command data length") + if mrl and mrl > 65536: + raise ValueError("invalid max response length") + if data: + apdu += pack(">xH", len(data)) + bytes(data) + if mrl > 0: + le = 0 if mrl == 65536 else mrl + apdu += pack(">H", le) if data else pack(">xH", le) + + apdu = self.transceive(apdu) + + if not apdu or len(apdu) < 2: + raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR) + + if check_status and apdu[-2:] != b"\x90\x00": + raise Type4TagCommandError.from_status(apdu[-2:]) + + return apdu[:-2] if check_status else apdu + + def __str__(self): + s = "{tag.__class__.__name__} MIU={tag._dep.miu} FWT={tag._dep.fwt:f}" + return s.format(tag=self) + + +class Type4ATag(Type4Tag): + def __init__(self, clf, target): + super(Type4ATag, self).__init__(clf, target) + self._nfcid = bytearray(target.sdd_res) + + log.debug("send RATS command to activate the Type 4A Tag") + if self.clf.max_recv_data_size < 256: + log.warning("{0} does not support fsd 256".format(self.clf)) + rats_cmd = bytearray.fromhex("E0 70") + else: + rats_cmd = bytearray.fromhex("E0 80") + rats_res = self.clf.exchange(rats_cmd, timeout=0.03) + log.debug("rcvd RATS response: {0}".format(hexlify(rats_res).decode())) + + fsci, fwti = rats_res[1] & 0x0F, rats_res[3] >> 4 + if fsci > 8: + log.warning("FSCI with RFU value in RATS_RES") + fsci = 8 + if fwti > 14: + log.warning("FWI with RFU value in RATS_RES") + fwti = 4 + + fsc = (16, 24, 32, 40, 48, 64, 96, 128, 256)[fsci] + fwt = 4096 / 13.56E6 * (2**fwti) + + if fsc > self.clf.max_send_data_size: + log.warning("{0} does not support fsc {1}".format(self.clf, fsc)) + fsc = self.clf.max_send_data_size + + log.debug("max command frame size is {0:d} byte".format(fsc)) + log.debug("max frame waiting time is {0:f}".format(fwt)) + + self._dep = IsoDepInitiator(clf, fsc, fwt) + self._extended_length_support = False + + +class Type4BTag(Type4Tag): + def __init__(self, clf, target): + super(Type4BTag, self).__init__(clf, target) + self._nfcid = bytearray(target.sensb_res[1:5]) + + log.debug("send ATTRIB command to activate the Type 4B Tag") + if self.clf.max_recv_data_size < 256: + log.warning("{0} does not support fsd 256".format(self.clf)) + attrib_cmd = b'\x1D' + self._nfcid + b'\x00\x07\x01\x00' + else: + attrib_cmd = b'\x1D' + self._nfcid + b'\x00\x08\x01\x00' + attrib_res = self.clf.exchange(attrib_cmd, timeout=0.03) + log.debug("rcvd ATTRIB response %s", hexlify(attrib_res).decode()) + + fsci, fwti = target.sensb_res[10] >> 4, target.sensb_res[11] >> 4 + if fsci > 8: + log.warning("FSCI with RFU value in SENSB_RES") + fsci = 8 + if fwti > 14: + log.warning("FWI with RFU value in SENSB_RES") + fwti = 4 + + fsc = (16, 24, 32, 40, 48, 64, 96, 128, 256)[fsci] + fwt = 4096 / 13.56E6 * (2**fwti) + + if fsc > self.clf.max_send_data_size: + log.warning("{0} does not support fsc {1}".format(self.clf, fsc)) + fsc = self.clf.max_send_data_size + + log.debug("max command frame size is {0:d} byte".format(fsc)) + log.debug("max frame waiting time is {0:f}".format(fwt)) + + self._dep = IsoDepInitiator(clf, fsc, fwt) + self._extended_length_support = False + + +def activate(clf, target): + if target.brty.endswith('A'): + return Type4ATag(clf, target) + if target.brty.endswith('B'): + return Type4BTag(clf, target) diff --git a/src/test/rfid.py b/src/test/rfid.py index 14caac2..8a2fa16 100644 --- a/src/test/rfid.py +++ b/src/test/rfid.py @@ -8,8 +8,8 @@ from pathlib import Path import serial import ndef -import nfc -from nfc.clf import RemoteTarget +from src.lib import nfc as nfc +from src.lib.nfc.clf import RemoteTarget logging.basicConfig( format="{asctime}:{name}:{levelname}:{message}", From 4bf753e50a7f90afaf539aa767bcbea602f92f33 Mon Sep 17 00:00:00 2001 From: gg Date: Tue, 24 Dec 2024 11:06:51 +0100 Subject: [PATCH 6/8] rfid imports --- src/lib/nfc/clf/device.py | 2 +- src/lib/nfc/clf/pn532.py | 2 +- src/lib/nfc/clf/pn53x.py | 10 +++++----- src/lib/nfc/tag/__init__.py | 28 ++++++++++++++-------------- src/lib/nfc/tag/tt2.py | 24 ++++++++++++------------ src/lib/nfc/tag/tt2_nxp.py | 10 +++++----- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/lib/nfc/clf/device.py b/src/lib/nfc/clf/device.py index 69c0622..e0c6695 100644 --- a/src/lib/nfc/clf/device.py +++ b/src/lib/nfc/clf/device.py @@ -105,7 +105,7 @@ def connect(path): for drv in drivers: for dev in devices: log.debug("trying {0} on {1}".format(drv, dev)) - driver = importlib.import_module("nfc.clf." + drv) + driver = importlib.import_module("src.lib.nfc.clf." + drv) tty = None try: tty = transport.TTY(dev) diff --git a/src/lib/nfc/clf/pn532.py b/src/lib/nfc/clf/pn532.py index 5ef8d91..d6469e1 100644 --- a/src/lib/nfc/clf/pn532.py +++ b/src/lib/nfc/clf/pn532.py @@ -52,7 +52,7 @@ listen_dep yes ========== ======= ============ """ -import nfc.clf +import src.lib.nfc.clf from . import pn53x import os diff --git a/src/lib/nfc/clf/pn53x.py b/src/lib/nfc/clf/pn53x.py index 6ef9fa0..f8495d5 100644 --- a/src/lib/nfc/clf/pn53x.py +++ b/src/lib/nfc/clf/pn53x.py @@ -26,7 +26,7 @@ interface chips, namely the NXP PN531, PN532, PN533 and the Sony RC-S956. """ -import nfc.clf +import src.lib.nfc.clf from . import device import os @@ -518,7 +518,7 @@ class Device(device.Device): self.log.debug("disable crc check for type 2 tag") rxmode = self.chipset.read_register("CIU_RxMode") self.chipset.write_register("CIU_RxMode", rxmode & 0x7F) - return nfc.clf.RemoteTarget( + return src.lib.nfc.clf.RemoteTarget( "106A", sens_res=sens_res, sel_res=sel_res, sdd_res=sdd_res) if self.chipset.read_register("CIU_FIFOData") == 0x26: @@ -668,15 +668,15 @@ class Device(device.Device): except Chipset.Error as error: self.log.debug(error) if error.errno == 1: - raise nfc.clf.TimeoutError + raise src.lib.nfc.clf.TimeoutError else: - raise nfc.clf.TransmissionError(str(error)) + raise src.lib.nfc.clf.TransmissionError(str(error)) except IOError as error: self.log.debug(error) if not error.errno == errno.ETIMEDOUT: raise error else: - raise nfc.clf.TimeoutError("send_cmd_recv_rsp") + raise src.lib.nfc.clf.TimeoutError("send_cmd_recv_rsp") def _tt1_send_cmd_recv_rsp(self, data, timeout): cname = self.__class__.__module__ + '.' + self.__class__.__name__ diff --git a/src/lib/nfc/tag/__init__.py b/src/lib/nfc/tag/__init__.py index 4cbfbf6..9229653 100644 --- a/src/lib/nfc/tag/__init__.py +++ b/src/lib/nfc/tag/__init__.py @@ -423,7 +423,7 @@ class TagCommandError(Exception): def activate(clf, target): - import nfc.clf + import src.lib.nfc.clf try: log.debug("trying to activate {0}".format(target)) if target.brty.endswith('A'): @@ -437,32 +437,32 @@ def activate(clf, target): return activate_tt4(clf, target) elif target.brty.endswith('F'): return activate_tt3(clf, target) - except nfc.clf.CommunicationError: + except src.lib.nfc.clf.CommunicationError: return None def activate_tt1(clf, target): log.debug("trying type 1 tag activation for {0}".format(target.brty)) - import nfc.tag.tt1 - return nfc.tag.tt1.activate(clf, target) + import src.lib.nfc.tag.tt1 + return src.lib.nfc.tag.tt1.activate(clf, target) def activate_tt2(clf, target): log.debug("trying type 2 tag activation for {0}".format(target.brty)) - import nfc.tag.tt2 - return nfc.tag.tt2.activate(clf, target) + import src.lib.nfc.tag.tt2 + return src.lib.nfc.tag.tt2.activate(clf, target) def activate_tt3(clf, target): log.debug("trying type 3 tag activation for {0}".format(target.brty)) - import nfc.tag.tt3 - return nfc.tag.tt3.activate(clf, target) + import src.lib.nfc.tag.tt3 + return src.lib.nfc.tag.tt3.activate(clf, target) def activate_tt4(clf, target): log.debug("trying type 4 tag activation for {0}".format(target.brty)) - import nfc.tag.tt4 - return nfc.tag.tt4.activate(clf, target) + import src.lib.nfc.tag.tt4 + return src.lib.nfc.tag.tt4.activate(clf, target) class TagEmulation(object): @@ -471,10 +471,10 @@ class TagEmulation(object): def emulate(clf, target): - import nfc.clf - assert isinstance(target, nfc.clf.LocalTarget) + import src.lib.nfc.clf + assert isinstance(target, src.lib.nfc.clf.LocalTarget) if target.tt3_cmd: - import nfc.tag.tt3 - return nfc.tag.tt3.Type3TagEmulation(clf, target) + import src.lib.nfc.tag.tt3 + return src.lib.nfc.tag.tt3.Type3TagEmulation(clf, target) else: log.debug("can't emulate with %s", target) diff --git a/src/lib/nfc/tag/tt2.py b/src/lib/nfc/tag/tt2.py index f26120c..52c3f93 100644 --- a/src/lib/nfc/tag/tt2.py +++ b/src/lib/nfc/tag/tt2.py @@ -25,7 +25,7 @@ from binascii import hexlify from struct import pack, unpack from . import Tag, TagCommandError -import nfc.clf +import src.lib.nfc.clf import logging log = logging.getLogger(__name__) @@ -489,7 +489,7 @@ class Type2Tag(Tag): self.target.sel_req = self.target.sdd_res[:] self._target = self.clf.sense(self.target) raise Type2TagCommandError( - INVALID_PAGE_ERROR if self.target else nfc.tag.RECEIVE_ERROR) + INVALID_PAGE_ERROR if self.target else src.lib.nfc.tag.RECEIVE_ERROR) if len(data) != 16: log.debug("invalid response %s", hexlify(data).decode()) @@ -583,7 +583,7 @@ class Type2Tag(Tag): # communication. If that failed (tag gone) then any # further attempt to transceive() is the same as # "unrecoverable timeout error". - raise Type2TagCommandError(nfc.tag.TIMEOUT_ERROR) + raise Type2TagCommandError(src.lib.nfc.tag.TIMEOUT_ERROR) started = time.time() error = None @@ -591,17 +591,17 @@ class Type2Tag(Tag): try: data = self.clf.exchange(data, timeout) break - except nfc.clf.CommunicationError as e: + except src.lib.nfc.clf.CommunicationError as e: error = e reason = error.__class__.__name__ log.debug("%s after %d retries" % (reason, retry)) else: - if type(error) is nfc.clf.TimeoutError: - raise Type2TagCommandError(nfc.tag.TIMEOUT_ERROR) - if type(error) is nfc.clf.TransmissionError: - raise Type2TagCommandError(nfc.tag.RECEIVE_ERROR) - if type(error) is nfc.clf.ProtocolError: - raise Type2TagCommandError(nfc.tag.PROTOCOL_ERROR) + if type(error) is src.lib.nfc.clf.TimeoutError: + raise Type2TagCommandError(src.lib.nfc.tag.TIMEOUT_ERROR) + if type(error) is src.lib.nfc.clf.TransmissionError: + raise Type2TagCommandError(src.lib.nfc.tag.RECEIVE_ERROR) + if type(error) is src.lib.nfc.clf.ProtocolError: + raise Type2TagCommandError(src.lib.nfc.tag.PROTOCOL_ERROR) raise RuntimeError("unexpected " + repr(error)) elapsed = time.time() - started @@ -686,8 +686,8 @@ def activate(clf, target): # sel_req we ensure that only the same tag will be found. target.sel_req = target.sdd_res[:] if target.sdd_res[0] == 0x04: # NXP - import nfc.tag.tt2_nxp - tag = nfc.tag.tt2_nxp.activate(clf, target) + import src.lib.nfc.tag.tt2_nxp + tag = src.lib.nfc.tag.tt2_nxp.activate(clf, target) if tag is not None: return tag else: diff --git a/src/lib/nfc/tag/tt2_nxp.py b/src/lib/nfc/tag/tt2_nxp.py index 1089990..623a5fc 100644 --- a/src/lib/nfc/tag/tt2_nxp.py +++ b/src/lib/nfc/tag/tt2_nxp.py @@ -19,7 +19,7 @@ # See the Licence for the specific language governing # permissions and limitations under the Licence. # ----------------------------------------------------------------------------- -import nfc.clf +import src.lib.nfc.clf from . import tt2 import os @@ -742,10 +742,10 @@ def activate(clf, target): return if rsp.startswith(b"\xAF"): return MifareUltralightC(clf, target) - except nfc.clf.TimeoutError: + except src.lib.nfc.clf.TimeoutError: if clf.sense(target) is None: return - except nfc.clf.CommunicationError as error: + except src.lib.nfc.clf.CommunicationError as error: log.debug(repr(error)) return @@ -761,10 +761,10 @@ def activate(clf, target): return NTAG203(clf, target) log.debug("no match for version %s", hexlify(rsp).decode().upper()) return - except nfc.clf.TimeoutError: + except src.lib.nfc.clf.TimeoutError: if clf.sense(target) is None: return - except nfc.clf.CommunicationError as error: + except src.lib.nfc.clf.CommunicationError as error: log.debug(repr(error)) return From 533d010881c80b5fe81b5c157b615ac69defbc1d Mon Sep 17 00:00:00 2001 From: edo-neo Date: Tue, 24 Dec 2024 11:44:57 +0100 Subject: [PATCH 7/8] dev --- src/lib/helpers/recipe_manager.py | 405 ++++++++++++++++++++++++++++++ 1 file changed, 405 insertions(+) create mode 100644 src/lib/helpers/recipe_manager.py diff --git a/src/lib/helpers/recipe_manager.py b/src/lib/helpers/recipe_manager.py new file mode 100644 index 0000000..7b6aa79 --- /dev/null +++ b/src/lib/helpers/recipe_manager.py @@ -0,0 +1,405 @@ +import os +import csv +import locale +from datetime import datetime +import shutil +from PyQt5.QtWidgets import QFileDialog +from lib.db import Recipes, db # Assuming these are part of your project structure + + +def read_steps(row, config, defaults=None, unsupported_steps=None): + if defaults is None: + defaults = config.get("recipes_defaults", lambda k: None) + + # Configurable fields from the config object + barcode_serial_field = config.get("recipe", {}).get("barcode_serial_field", "codice_a_barre").strip() + warning_image_field = config.get("recipe", {}).get("warning_image_field", "warning_img").strip() + print_template_field = config.get("recipe", {}).get("label_template_field", "modello_etichetta").strip() + decsep = locale.localeconv()["decimal_point"] + + # Extract and clean "r nominale" value + rcsv = ( + row.get("r nominale", defaults["r nominale"]) + .replace(" ", "").replace(",", decsep).replace("Ω", "").replace("?", "") + ) + if rcsv == "": + rcsv = "999" # Default fallback for "r nominale" if empty + + # Helper functions + def get_default_value(field, key): + value = field.get(key, defaults[key]) + return value if value != "" else defaults[key] + + def safe_parse(value): + try: + return int(float(value)) + except ValueError: + return 0 # Default to 0 if parsing fails + + # Define the steps dictionary + steps = { + "count": { + "amount": row.get("dimensione_lotto", defaults["dimensione_lotto"]), + "warning_img": row.get(warning_image_field, defaults["warning_img"]), + "require_discard_piece": row.get("richiedi_inserimento_scarto", defaults["richiedi_inserimento_scarto"]), + }, + "connector": { + "connector": row.get("connettore", defaults["connettore"]), + }, + "barcodes": { + "serial": row.get(barcode_serial_field, defaults["codice_a_barre"]), + "n_pieces": row.get("n_componenti", defaults["n_componenti"]), + "barcode_input_2": row.get("barcode_input_2", "-"), + "barcode_input_3": row.get("barcode_input_3", "-"), + "barcode_input_4": row.get("barcode_input_4", "-"), + "barcode_input_5": row.get("barcode_input_5", "-"), + }, + "resistance": { + "scale": locale.atof(row.get("scala_resistenza", defaults["scala_resistenza"])), + "expected": locale.atof(rcsv), + "tolerance_pos": locale.atof(get_default_value(row, "tolleranza_resistenza_pos")), + "tolerance_neg": locale.atof(get_default_value(row, "tolleranza_resistenza_neg")), + }, + "screws": { + "quantity": row.get("viti", defaults["viti"]), + }, + "instruction": {}, # Empty placeholder for future extensions + "leak_1": { + "pre_filling_time": safe_parse(row.get("tempo_pre_riempimento", defaults["tempo_pre_riempimento"])), + "pre_filling_pressure": safe_parse( + row.get("pressione_pre_riempimento", defaults["pressione_pre_riempimento"])), + "filling_time": safe_parse(row.get("tempo_riempimento", defaults["tempo_riempimento"])), + "settling_time": safe_parse(get_default_value(row, "tempo_assestamento")), + "settling_pressure_min_percent": safe_parse( + row.get("percentuale_minima_pressione_assestamento", + defaults["percentuale_minima_pressione_assestamento"]) + ), + "settling_pressure_max_percent": safe_parse( + row.get("percentuale_massima_pressione_assestamento", + defaults["percentuale_massima_pressione_assestamento"]) + ), + "test_time": safe_parse(row.get("tempo_di_test", defaults["tempo_di_test"])), + "test_pressure_qneg": safe_parse( + row.get("pressione_di_test_delta_minimo", defaults["pressione_di_test_delta_minimo"])), + "test_pressure": safe_parse(row.get("pressione_di_test", defaults["pressione_di_test"])), + "test_pressure_qpos": safe_parse( + row.get("pressione_di_test_delta_massimo", defaults["pressione_di_test_delta_massimo"])), + "flush_time": safe_parse(row.get("tempo_svuotamento", defaults["tempo_svuotamento"])), + "flush_pressure": safe_parse(row.get("pressione_svuotamento", defaults["pressione_svuotamento"])), + "chan_sel": safe_parse(row.get("canale_di_prova", defaults["canale_di_prova"])), + "ext_flush_time": safe_parse(row.get("tempo_svuotamento_esterno", defaults["tempo_svuotamento_esterno"])), + "ext_blow_time": safe_parse(row.get("tempo_soffiaggio_esterno", defaults["tempo_soffiaggio_esterno"])), + }, + "leak_2": { + "pre_filling_time": safe_parse(row.get("tempo_pre_riempimento_2", defaults["tempo_pre_riempimento_2"])), + "pre_filling_pressure": safe_parse( + row.get("pressione_pre_riempimento_2", defaults["pressione_pre_riempimento_2"])), + "filling_time": safe_parse(row.get("tempo_riempimento_2", defaults["tempo_riempimento_2"])), + "settling_time": safe_parse(row.get("tempo_assestamento_2", defaults["tempo_assestamento_2"])), + "settling_pressure_min_percent": safe_parse( + row.get("percentuale_minima_pressione_assestamento_2", + defaults["percentuale_minima_pressione_assestamento_2"]) + ), + "settling_pressure_max_percent": safe_parse( + row.get("percentuale_massima_pressione_assestamento_2", + defaults["percentuale_massima_pressione_assestamento_2"]) + ), + "test_time": safe_parse(row.get("tempo_di_test_2", defaults["tempo_di_test_2"])), + "test_pressure_qneg": safe_parse( + row.get("pressione_di_test_delta_minimo_2", defaults["pressione_di_test_delta_minimo_2"])), + "test_pressure": safe_parse(row.get("pressione_di_test_2", defaults["pressione_di_test_2"])), + "test_pressure_qpos": safe_parse( + row.get("pressione_di_test_delta_massimo_2", defaults["pressione_di_test_delta_massimo_2"])), + "flush_time": safe_parse(row.get("tempo_svuotamento_2", defaults["tempo_svuotamento_2"])), + "flush_pressure": safe_parse(row.get("pressione_svuotamento_2", defaults["pressione_svuotamento_2"])), + "chan_sel": safe_parse(row.get("canale_di_prova_2", defaults["canale_di_prova_2"])), + "ext_flush_time": safe_parse(row.get("tempo_svuotamento_esterno_2", defaults["tempo_svuotamento_esterno"])), + "ext_blow_time": safe_parse(row.get("tempo_soffiaggio_esterno_2", defaults["tempo_soffiaggio_esterno"])), + }, + "vision": { + "recipe": row.get("ricetta_visione", defaults["ricetta_visione"]), + }, + "print": { + "template": row.get(print_template_field, defaults["modello_etichetta"]), + "labeltxt_1": row.get("testo_etich_1", ""), + "labeltxt_2": row.get("testo_etich_2", ""), + "labeltxt_3": row.get("testo_etich_3", ""), + "labeltxt_4": row.get("testo_etich_4", ""), + "labeltxt_5": row.get("barcode_input_finelinea", ""), + "extra_label": row.get("etichette_supplementari", ""), + }, + } + + # Remove unsupported steps if specified + if unsupported_steps: + for step in unsupported_steps: + steps.pop(step, None) + + return steps + + + +def import_recipes(config, csv_path=None, defaults=None, unsupported_steps=None, logger=None): + """ + Import recipes from CSV and update or create new ones in the database. + + :param config: Configuration object with recipe settings. + :param csv_path: Path to the CSV file (optional). If None, a file dialog will open. + :param defaults: Default values to use for missing fields in the CSV. + :param unsupported_steps: A list of unsupported step names to exclude. + :param logger: Logger object for logging messages (optional). + """ + if defaults is None: + defaults = config.get("recipes_defaults", lambda k: None) + + # Open file dialog if csv_path is not provided + if csv_path is None: + options = QFileDialog.Options() + options |= QFileDialog.DontUseNativeDialog + csv_path, _ = QFileDialog.getOpenFileName( + None, + "Import Recipes", + "recipes.csv", + "CSV files (*.csv);;All Files (*)", + options=options, + ) + csv_path = str(csv_path) + if not len(csv_path): + return + + if logger: + logger.info(f"Importing recipes from: {csv_path}.") + + # Get field mappings from the config + recipe_name_field = config.get("recipe", {}).get("recipe_name_field", "codice_ricetta").strip() + part_number_field = config.get("recipe", {}).get("part_number_field", "part_number").strip() + description_field = config.get("recipe", {}).get("description_field", "descrizione").strip() + barcode_enable_field = config.get( + "recipe", {} + ).get("barcode_enable_field", "verifica_codice_a_barre_abilitata").strip() + + with open(csv_path, "r", encoding="utf-8-sig") as file: + reader = csv.DictReader(file) + count = 0 + + for ucrow in reader: + # Normalize row keys to lowercase for consistency + row = dict((k.lower(), v) for k, v in ucrow.items()) + recipe_name = row.get(recipe_name_field, defaults["codice_ricetta"]) + steps_specs = read_steps(row, config, defaults=defaults, unsupported_steps=unsupported_steps) + + # Create or update recipe in the database + try: + # Try to fetch existing recipe + recipe = Recipes.get_by_id(recipe_name) + recipe_is_new = False + except Recipes.DoesNotExist: + # Create a new recipe if it doesn't exist + recipe = Recipes(name=recipe_name, part_number="TEMPORARY") + recipe_is_new = True + + # Update recipe attributes + recipe.client = row.get("cliente", defaults["cliente"]) + recipe.part_number = row.get(part_number_field, defaults["part_number"]) + recipe.description = row.get(description_field, defaults["descrizione"]) + + # Recipe specifications + steps = {} + for step_name, step_spec in steps_specs.items(): + if unsupported_steps is None or step_name not in unsupported_steps: + steps[step_name] = step_spec + + recipe.spec = { + "count": len( + row.get("dimensione_lotto_abilitata", defaults["dimensione_lotto_abilitata"])) and "count" not in ( + unsupported_steps or []), + "connector": len(row.get("verifica_connettore_abilitata", + defaults["verifica_connettore_abilitata"])) and "connector" not in ( + unsupported_steps or []), + "barcodes": len(row.get(barcode_enable_field, + defaults["verifica_codice_a_barre_abilitata"])) and "barcodes" not in ( + unsupported_steps or []), + "resistance": len(row.get("verifica_resistenza_connettore_abilitata", defaults[ + "verifica_resistenza_connettore_abilitata"])) and "resistance" not in (unsupported_steps or []), + "screws": len(row.get("avvitatura_abilitata", defaults["avvitatura_abilitata"])) and "screws" not in ( + unsupported_steps or []), + "instruction": len( + row.get("istruzione_abilitata", defaults["istruzione_abilitata"])) and "instruction" not in ( + unsupported_steps or []), + "instruction_extra": len(row.get("istruzione_abilitata_extra", defaults[ + "istruzione_abilitata_extra"])) and "instruction_extra" not in (unsupported_steps or []), + "leak_1": len( + row.get("prova_tenuta_abilitata", defaults["prova_tenuta_abilitata"])) and "leak_1" not in ( + unsupported_steps or []), + "leak_2": len( + row.get("prova_tenuta_abilitata_2", defaults["prova_tenuta_abilitata_2"])) and "leak_2" not in ( + unsupported_steps or []), + "vision": len( + row.get("test_visione_abilitato", defaults["test_visione_abilitato"])) and "vision" not in ( + unsupported_steps or []), + "print": len( + row.get("stampa_etichetta_abilitata", defaults["stampa_etichetta_abilitata"])) and "print" not in ( + unsupported_steps or []), + "steps": steps_specs, + } + + if recipe_is_new: + recipe.save(force_insert=True) # Insert new recipe + else: + recipe.save() # Update existing recipe + + count += 1 # Increment imported recipe count + + db.commit() # Commit all changes to the database + + if logger: + logger.info(f"Imported {count} recipes.") + + + +def export_recipes(config, csv_path=None, logger=None): + if csv_path is None: + options = QFileDialog.Options() + options |= QFileDialog.DontUseNativeDialog + csv_path, _ = QFileDialog.getSaveFileName( + None, + "Export Recipes", + "recipes.csv", + "CSV files (*.csv);;All Files (*)", + options=options, + ) + csv_path = str(csv_path) + if not len(csv_path): + return + + if not csv_path.lower().endswith(".csv"): + csv_path += ".csv" + os.makedirs(os.path.dirname(csv_path), exist_ok=True) + + recipe_name_field = config.get("recipe", {}).get("recipe_name_field", "codice_ricetta").strip() + barcode_enable_field = config.get("recipe", {}).get("barcode_enable_field", + "verifica_codice_a_barre_abilitata").strip() + barcode_serial_field = config.get("recipe", {}).get("barcode_serial_field", "codice_a_barre").strip() + print_template_field = config.get("recipe", {}).get("label_template_field", "modello_etichetta").strip() + data = [] + fieldnames = set() # Use a set to avoid duplicates + + # Iterate over all recipes in the database + for recipe in Recipes.select(): + steps = recipe.get_steps_map() + exportable = { + # Base fields + recipe_name_field: recipe.name, + "cliente": recipe.client, + "part_number": recipe.part_number, + } + + # Add base fields to the fieldnames + fieldnames.update([recipe_name_field, "cliente", "part_number"]) + + # Check and add steps conditionally + if "connector" in steps: + exportable.update({ + "verifica_connettore_abilitata": "x", + "connettore": steps["connector"].spec["connector"] + }) + fieldnames.update(["verifica_connettore_abilitata", "connettore"]) + + if "resistance" in steps: + exportable.update({ + "verifica_resistenza_connettore_abilitata": "x", + "scala_resistenza": steps["resistance"].spec["scale"], + "r nominale": steps["resistance"].spec["expected"], + "tolleranza_resistenza_pos": steps["resistance"].spec["tolerance_pos"], + "tolleranza_resistenza_neg": steps["resistance"].spec["tolerance_neg"], + }) + fieldnames.update(["verifica_resistenza_connettore_abilitata", "scala_resistenza", "r nominale", + "tolleranza_resistenza_pos", "tolleranza_resistenza_neg"]) + + if "barcodes" in steps: + exportable.update({ + barcode_enable_field: "x", + barcode_serial_field: steps["barcodes"].spec["serial"] + }) + fieldnames.update([barcode_enable_field, barcode_serial_field]) + + if "screws" in steps: + exportable.update({ + "avvitatura_abilitata": "x", + "viti": steps["screws"].spec["quantity"] + }) + fieldnames.update(["avvitatura_abilitata", "viti"]) + + if "leak_1" in steps: + exportable.update({ + "prova_tenuta_abilitata": "x", + "tempo_pre_riempimento": steps["leak_1"].spec["pre_filling_time"], + "pressione_pre_riempimento": steps["leak_1"].spec["pre_filling_pressure"], + "tempo_di_test": steps["leak_1"].spec["test_time"], + "pressione_di_test": steps["leak_1"].spec["test_pressure"], + }) + fieldnames.update(["prova_tenuta_abilitata", "tempo_pre_riempimento", "pressione_pre_riempimento", + "tempo_di_test", "pressione_di_test"]) + + if "leak_2" in steps: + exportable.update({ + "prova_tenuta_abilitata_2": "x", + "tempo_pre_riempimento_2": steps["leak_2"].spec["pre_filling_time"], + "pressione_pre_riempimento_2": steps["leak_2"].spec["pre_filling_pressure"], + "tempo_di_test_2": steps["leak_2"].spec["test_time"], + "pressione_di_test_2": steps["leak_2"].spec["test_pressure"], + }) + fieldnames.update(["prova_tenuta_abilitata_2", "tempo_pre_riempimento_2", "pressione_pre_riempimento_2", + "tempo_di_test_2", "pressione_di_test_2"]) + + if "vision" in steps: + exportable.update({ + "test_visione_abilitato": steps["vision"].spec.get("enabled", ""), + "ricetta_visione": steps["vision"].spec["recipe"] + }) + fieldnames.update(["test_visione_abilitato", "ricetta_visione"]) + + if "print" in steps: + exportable.update({ + "stampa_etichetta_abilitata": "x", + print_template_field: steps["print"].spec["template"], + }) + fieldnames.update(["stampa_etichetta_abilitata", print_template_field]) + + # Append the exportable row to the data + data.append(exportable) + + # Export data to CSV if there is any data + if len(data): + if logger: + logger.info(f"Exporting recipes to {csv_path}") + with open(csv_path, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=list(fieldnames)) + writer.writeheader() + writer.writerows(data) + if logger: + logger.info(f"Exported {len(data)} recipes to {csv_path}.") + + +def backup_current_recipes(config, logger=None): + """ + Back up current recipes to a timestamped CSV file in the predefined backup directory. + """ + # Define the backup directory and file name + backup_dir = os.path.join('config', 'csv_import', 'backup_csv') + timestamp = datetime.now().strftime("%d%m%y%H%M%S") + backup_file = f"backup_{timestamp}.csv" + backup_path = os.path.join(backup_dir, backup_file) + + # Ensure the backup directory exists + os.makedirs(backup_dir, exist_ok=True) + + # Export current recipes to the backup path + export_recipes(config=config, csv_path=backup_path, logger=logger) + + if logger: + logger.info(f"Backup created at: {backup_path}") + + return backup_path # Return the backup path for reference if needed + + From 7c79bfc6a3fe9925505884caeedacfa0a7572106 Mon Sep 17 00:00:00 2001 From: gg Date: Tue, 24 Dec 2024 12:30:17 +0100 Subject: [PATCH 8/8] rfid imports --- src/components/rfid_pn532.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/rfid_pn532.py b/src/components/rfid_pn532.py index a066754..241d79e 100644 --- a/src/components/rfid_pn532.py +++ b/src/components/rfid_pn532.py @@ -5,8 +5,8 @@ import platform from PyQt5.QtCore import QMutex, Qt, QTimer, pyqtSlot, pyqtSignal from .component import Component import ndef -import nfc -from nfc.clf import RemoteTarget +import src.lib.nfc +from src.lib.nfc.clf import RemoteTarget class RFID_PN532(Component): @@ -26,7 +26,7 @@ class RFID_PN532(Component): self._period = 1 def open_device(self): - self.clf = nfc.ContactlessFrontend() + self.clf = src.lib.nfc.ContactlessFrontend() for dev in self.dev_list: self.connected = self.clf.open(dev) if self.connected: @@ -52,7 +52,7 @@ class RFID_PN532(Component): else: target = self.clf.sense(RemoteTarget('106A'), RemoteTarget('106B'), RemoteTarget('212F')) if target is not None: - tag = nfc.tag.activate(self.clf, target) + tag = src.lib.nfc.tag.activate(self.clf, target) if tag is not None: self.log.debug("tag present") if tag.ndef is not None: