Merge remote-tracking branch 'origin/master'

This commit is contained in:
Utente 2025-01-07 08:49:12 +01:00
commit 755ce179b0
50 changed files with 17792 additions and 268 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 MiB

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="1200"
height="700"
viewBox="0 0 1200 700"
version="1.1"
id="svg5"
xml:space="preserve"
inkscape:version="1.2.2 (1:1.2.2+202212051550+b0a8486541)"
sodipodi:docname="DEFAULT.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="1.0570734"
inkscape:cx="365.15913"
inkscape:cy="357.59106"
inkscape:window-width="2560"
inkscape:window-height="1023"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs2" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"><text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ff7f2a;fill-opacity:1;stroke:#aa4400"
x="277.31964"
y="373.9111"
id="text226"><tspan
sodipodi:role="line"
id="tspan224"
x="277.31964"
y="373.9111"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:53.3333px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke:#aa4400;fill:#ff7f2a">DISEGNO NON DISPONIBILE</tspan></text></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -14,10 +14,11 @@ remote_api: absent
tecna_t3: present
vision_saver: absent
vision: absent
screwdriver: absent
#fixture_id: present
screwdriver: present
fixture_id: present
digital_io: present
external_flush_blow: absent
barcode_recipe_selection: present
# VERO PROJECT LOCAL SERVER
[archive_synchronizer_extra]
@ -104,9 +105,9 @@ settling_pressure_min_percent: 5
settling_pressure_max_percent: 5
test_pressure: 7000
test_time: 10
test_pressure_qpos: 10 #Q+ Upper test leak limit
test_pressure_qneg: 30 #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

View File

@ -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,12 +152,54 @@ class ArchiveSynchronizer(Component):
actions = [actions]
for action in actions:
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")
local_path = action.get("local_path")
self.log.info(f"Executing remote fetch with remote_path: {remote_path} and local_path: {local_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")
except Exception as e:
@ -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:

View File

@ -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:

1
src/lib/__init__.py Normal file
View File

@ -0,0 +1 @@

View File

@ -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

47
src/lib/nfc/__init__.py Normal file
View File

@ -0,0 +1,47 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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"
###############################################################################

214
src/lib/nfc/__main__.py Normal file
View File

@ -0,0 +1,214 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2016 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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())

1251
src/lib/nfc/clf/__init__.py Normal file

File diff suppressed because it is too large Load Diff

242
src/lib/nfc/clf/acr122.py Normal file
View File

@ -0,0 +1,242 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2011, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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("<BI5B", 0x6F, len(data), 0, 0, 0, 0, 0) + data
self.transport.write(bytearray(frame))
frame = self.transport.read(int(timeout * 1000))
if not frame or len(frame) < 10:
log.error("insufficient data for decoding ccid response")
raise IOError(errno.EIO, os.strerror(errno.EIO))
if frame[0] != 0x80:
log.error("expected a RDR_to_PC_DataBlock")
raise IOError(errno.EIO, os.strerror(errno.EIO))
if len(frame) != 10 + struct.unpack("<I", memoryview(frame)[1:5])[0]:
log.error("RDR_to_PC_DataBlock length mismatch")
raise IOError(errno.EIO, os.strerror(errno.EIO))
return frame[10:]
def command(self, cmd_code, cmd_data, timeout):
"""Send a host command and return the chip response.
"""
log.log(logging.DEBUG-1, "{} {}".format(self.CMD[cmd_code],
hexlify(cmd_data).decode()))
frame = bytearray([0xD4, cmd_code]) + bytearray(cmd_data)
frame = bytearray([0xFF, 0x00, 0x00, 0x00, len(frame)]) + frame
frame = self.ccid_xfr_block(frame, timeout)
if not frame or len(frame) < 4:
log.error("insufficient data for decoding chip response")
raise IOError(errno.EIO, os.strerror(errno.EIO))
if not (frame[0] == 0xD5 and frame[1] == cmd_code + 1):
log.error("received invalid chip response")
raise IOError(errno.EIO, os.strerror(errno.EIO))
if not (frame[-2] == 0x90 and frame[-1] == 0x00):
log.error("received pseudo apdu with error status")
raise IOError(errno.EIO, os.strerror(errno.EIO))
return frame[2:-2]

105
src/lib/nfc/clf/arygon.py Normal file
View File

@ -0,0 +1,105 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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))

660
src/lib/nfc/clf/device.py Normal file
View File

@ -0,0 +1,660 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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("src.lib.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

316
src/lib/nfc/clf/pn531.py Normal file
View File

@ -0,0 +1,316 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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

454
src/lib/nfc/clf/pn532.py Normal file
View File

@ -0,0 +1,454 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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 src.lib.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))

399
src/lib/nfc/clf/pn533.py Normal file
View File

@ -0,0 +1,399 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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

1064
src/lib/nfc/clf/pn53x.py Normal file

File diff suppressed because it is too large Load Diff

986
src/lib/nfc/clf/rcs380.py Normal file
View File

@ -0,0 +1,986 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2012, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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("<H", bytes(frame[5:7]))[0]
self._data = frame[8:8+length]
else:
frame = bytearray([0, 0, 255, 255, 255])
frame += bytearray(struct.pack("<H", len(data)))
frame += bytearray(struct.pack("B", (256 - sum(frame[5:7])) % 256))
frame += bytearray(data)
frame += bytearray([(256 - sum(frame[8:])) % 256, 0])
self._frame = frame
def __str__(self):
return str(self._frame)
def __bytes__(self):
return bytes(self._frame)
@property
def type(self):
return self._type
@property
def data(self):
return self._data
class CommunicationError(Exception):
err2str = {0x00000000: "NO_ERROR",
0x00000001: "PROTOCOL_ERROR",
0x00000002: "PARITY_ERROR",
0x00000004: "CRC_ERROR",
0x00000008: "COLLISION_ERROR",
0x00000010: "OVERFLOW_ERROR",
0x00000040: "TEMPERATURE_ERROR",
0x00000080: "RECEIVE_TIMEOUT_ERROR",
0x00000100: "CRYPTO1_ERROR",
0x00000200: "RFCA_ERROR",
0x00000400: "RF_OFF_ERROR",
0x00000800: "TRANSMIT_TIMEOUT_ERROR",
0x80000000: "RECEIVE_LENGTH_ERROR"
}
str2err = dict([(v, k) for k, v in err2str.items()])
def __init__(self, status_bytes):
self.errno = struct.unpack('<L', status_bytes)[0]
def __eq__(self, strerr):
errno = CommunicationError.str2err[strerr]
return bool(self.errno & errno) if self.errno or errno else True
def __ne__(self, strerr):
return not self.__eq__(strerr)
def __str__(self):
return self.__class__.__name__ + ' ' + CommunicationError.err2str.get(
self.errno, "0x{0:08X}".format(self.errno))
class StatusError(Exception):
err2str = ("SUCCESS", "PARAMETER_ERROR", "PB_ERROR", "RFCA_ERROR",
"TEMPERATURE_ERROR", "PWD_ERROR", "RECEIVE_ERROR",
"COMMANDTYPE_ERROR")
def __init__(self, status):
self.errno = status
def __str__(self):
try:
return StatusError.err2str[self.errno]
except IndexError:
return "UNKNOWN STATUS ERROR 0x{:02X}".format(self.errno)
class Chipset(object):
ACK = bytearray.fromhex('0000FF00FF00')
CMD = {
# RF Communication
0x00: "InSetRF",
0x02: "InSetProtocol",
0x04: "InCommRF",
0x06: "SwitchRF",
0x10: "MaintainFlash",
0x12: "ResetDevice",
0x20: "GetFirmwareVersion",
0x22: "GetPDDataVersion",
0x24: "GetProperty",
0x26: "InGetProtocol",
0x28: "GetCommandType",
0x2A: "SetCommandType",
0x30: "InSetRCT",
0x32: "InGetRCT",
0x34: "GetPDData",
0x36: "ReadRegister",
0x40: "TgSetRF",
0x42: "TgSetProtocol",
0x44: "TgSetAuto",
0x46: "TgSetRFOff",
0x48: "TgCommRF",
0x50: "TgGetProtocol",
0x60: "TgSetRCT",
0x62: "TgGetRCT",
0xF0: "Diagnose",
}
def __init__(self, transport, logger):
self.transport = transport
self.log = logger
# write ack to perform a soft reset
# raises IOError(EACCES) if we're second
self.transport.write(Chipset.ACK)
# Clear any response data that may be leftover from the last
# session when it was killed.
try:
while True:
data = self.transport.read(timeout=10)
log.debug("cleared garbage %s", hexlify(data).decode())
except IOError:
pass
# do some basic initialization and deactivate rf
self.set_command_type(1)
self.get_firmware_version()
self.get_pd_data_version()
self.switch_rf("off")
def close(self):
self.switch_rf('off')
self.transport.write(Chipset.ACK)
self.transport.close()
self.transport = None
def send_command(self, cmd_code, cmd_data):
cmd_data = bytearray(cmd_data)
log.log(logging.DEBUG-1, "{} {}".format(self.CMD[cmd_code],
hexlify(cmd_data).decode()))
if self.transport is not None:
cmd = bytearray([0xD6, cmd_code]) + cmd_data
self.transport.write(bytes(Frame(cmd)))
ack = Frame(self.transport.read())
if ack.type == 'ack':
rsp = Frame(self.transport.read())
if rsp.type == 'data':
if rsp.data[0] == 0xD7 and rsp.data[1] == cmd_code + 1:
return rsp.data[2:]
else:
logmsg = "expected rsp code D7{:02X} not {:02X}{:02X}"
log.error(logmsg.format(cmd_code+1, *rsp.data[0:2]))
else:
log.error("expected data but got {}".format(rsp.type))
else:
log.error("expected ack but got {}".format(ack.type))
else:
log.debug("transport closed in send_command")
def in_set_rf(self, brty_send, brty_recv=None):
settings = {
"212F": (1, 1, 15, 1), "424F": (1, 2, 15, 2),
"106A": (2, 3, 15, 3), "212A": (4, 4, 15, 4),
"424A": (5, 5, 15, 5), "106B": (3, 7, 15, 7),
"212B": (3, 8, 15, 8), "424B": (3, 9, 15, 9),
}
if brty_recv is None:
brty_recv = brty_send
data = settings[brty_send][0:2] + settings[brty_recv][2:4]
data = self.send_command(0x00, data)
if data and data[0] != 0:
raise StatusError(data[0])
in_set_protocol_defaults = bytearray.fromhex(
"0018 0101 0201 0300 0400 0500 0600 0708 0800 0900"
"0A00 0B00 0C00 0E04 0F00 1000 1100 1200 1306")
def in_set_protocol(self, data=None, **kwargs):
data = bytearray() if data is None else bytearray(data)
KEYS = ("initial_guard_time", "add_crc", "check_crc", "multi_card",
"add_parity", "check_parity", "bitwise_anticoll",
"last_byte_bit_count", "mifare_crypto", "add_sof",
"check_sof", "add_eof", "check_eof", "rfu", "deaf_time",
"continuous_receive_mode", "min_len_for_crm",
"type_1_tag_rrdd", "rfca", "guard_time")
for key, value in sorted(kwargs.items()):
data.extend(bytearray([KEYS.index(key), int(value)]))
if len(data) > 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("<H", timeout) + bytes(data))
if data and tuple(data[0:4]) != (0, 0, 0, 0):
raise CommunicationError(data[0:4])
return data[5:] if data else None
def switch_rf(self, switch):
switch = ("off", "on").index(switch)
data = self.send_command(0x06, [switch])
if data and data[0] != 0:
raise StatusError(data[0])
def tg_set_rf(self, comm_type):
tg_comm_type = {"106A": (8, 11), "212F": (8, 12), "424F": (8, 13),
"212A": (8, 14), "424A": (8, 15)}
comm_type = tg_comm_type[comm_type]
data = self.send_command(0x40, comm_type)
if data and data[0] != 0:
raise StatusError(data[0])
tg_set_protocol_defaults = bytearray.fromhex("0001 0101 0207")
def tg_set_protocol(self, data=None, **kwargs):
data = bytearray() if data is None else bytearray(data)
KEYS = ("send_timeout_time_unit", "rf_off_error",
"continuous_receive_mode")
for key, value in sorted(kwargs.items()):
data.extend(bytearray([KEYS.index(key), int(value)]))
data = self.send_command(0x42, bytearray(data))
if data and data[0] != 0:
raise StatusError(data[0])
def tg_set_auto(self, data):
data = self.send_command(0x44, data)
if data and data[0] != 0:
raise StatusError(data[0])
def tg_comm_rf(self, guard_time=0, send_timeout=0xFFFF,
mdaa=False, nfca_params=b'', nfcf_params=b'',
mf_halted=False, arae=False, recv_timeout=0,
transmit_data=None):
# Send a response packet and receive the next request. If
# *transmit_data* is None skip sending. If *recv_timeout* is
# zero skip receiving. Data is sent only between *guard_time*
# and *send_timeout*, measured from the end of the last
# received data. If *mdaa* is True, reply to Type A and Type F
# activation commands with *nfca_params* (sens_res, nfcid1-3,
# sel_res) and *nfcf_params* (idm, pmm, system_code).
data = struct.pack("<HH?6s18s??H", guard_time, send_timeout,
mdaa, bytes(nfca_params), bytes(nfcf_params),
mf_halted, arae, recv_timeout)
if transmit_data:
data = data + bytes(transmit_data)
data = self.send_command(0x48, data)
if data and tuple(data[3:7]) != (0, 0, 0, 0):
raise CommunicationError(data[3:7])
return data
def reset_device(self, startup_delay=0):
self.send_command(0x12, struct.pack("<H", startup_delay))
self.transport.write(Chipset.ACK)
time.sleep(float(startup_delay + 500)/1000)
def get_firmware_version(self, option=None):
assert option in (None, 0x60, 0x61, 0x80)
data = self.send_command(0x20, [option] if option else [])
log.debug("firmware version {1:x}.{0:02x}".format(*data))
return data
def get_pd_data_version(self):
data = self.send_command(0x22, [])
log.debug("package data format {1:x}.{0:02x}".format(*data))
def get_command_type(self):
data = self.send_command(0x28, [])
return struct.unpack(">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

376
src/lib/nfc/clf/rcs956.py Normal file
View File

@ -0,0 +1,376 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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

View File

@ -0,0 +1,345 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2012, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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))

577
src/lib/nfc/clf/udp.py Normal file
View File

@ -0,0 +1,577 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2012, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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:<host>:<port>``
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

895
src/lib/nfc/dep.py Normal file
View File

@ -0,0 +1,895 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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'

View File

@ -0,0 +1,29 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2012 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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

View File

@ -0,0 +1,118 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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()

View File

@ -0,0 +1,128 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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')]

View File

@ -0,0 +1,38 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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

42
src/lib/nfc/llcp/err.py Normal file
View File

@ -0,0 +1,42 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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)

886
src/lib/nfc/llcp/llc.py Normal file
View File

@ -0,0 +1,886 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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

945
src/lib/nfc/llcp/pdu.py Normal file
View File

@ -0,0 +1,945 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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()

542
src/lib/nfc/llcp/sec.py Normal file
View File

@ -0,0 +1,542 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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)

177
src/lib/nfc/llcp/socket.py Normal file
View File

@ -0,0 +1,177 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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)

733
src/lib/nfc/llcp/tco.py Normal file
View File

@ -0,0 +1,733 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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

View File

@ -0,0 +1,36 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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

247
src/lib/nfc/snep/client.py Normal file
View File

@ -0,0 +1,247 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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]

175
src/lib/nfc/snep/server.py Normal file
View File

@ -0,0 +1,175 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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

480
src/lib/nfc/tag/__init__.py Normal file
View File

@ -0,0 +1,480 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2013, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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 <https://ndeflib.readthedocs.io>`_ 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 <https://ndeflib.readthedocs.io>`_ 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 src.lib.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 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 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 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 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 src.lib.nfc.tag.tt4
return src.lib.nfc.tag.tt4.activate(clf, target)
class TagEmulation(object):
"""Base class for tag emulation classes."""
pass
def emulate(clf, target):
import src.lib.nfc.clf
assert isinstance(target, src.lib.nfc.clf.LocalTarget)
if target.tt3_cmd:
import src.lib.nfc.tag.tt3
return src.lib.nfc.tag.tt3.Type3TagEmulation(clf, target)
else:
log.debug("can't emulate with %s", target)

555
src/lib/nfc/tag/tt1.py Normal file
View File

@ -0,0 +1,555 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2011, 2017
# Stephen Tiedemann <stephen.tiedemann@gmail.com>
# Alexander Knaub <sanyok.og@googlemail.com>
#
# 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)

View File

@ -0,0 +1,159 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2014, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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)

697
src/lib/nfc/tag/tt2.py Normal file
View File

@ -0,0 +1,697 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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 src.lib.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 src.lib.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(src.lib.nfc.tag.TIMEOUT_ERROR)
started = time.time()
error = None
for retry in range(1 + retries):
try:
data = self.clf.exchange(data, timeout)
break
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 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
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 src.lib.nfc.tag.tt2_nxp
tag = src.lib.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)

771
src/lib/nfc/tag/tt2_nxp.py Normal file
View File

@ -0,0 +1,771 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2014, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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
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 src.lib.nfc.clf.TimeoutError:
if clf.sense(target) is None:
return
except src.lib.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 src.lib.nfc.clf.TimeoutError:
if clf.sense(target) is None:
return
except src.lib.nfc.clf.CommunicationError as error:
log.debug(repr(error))
return
return MifareUltralight(clf, target)

930
src/lib/nfc/tag/tt3.py Normal file
View File

@ -0,0 +1,930 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2009, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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("<H", (sn & 0x3ff) << 6 | (sa & 0x3f))
@classmethod
def unpack(cls, s):
"""Unpack and return a ServiceCode from a byte string."""
v = unpack("<H", s[0:2])[0]
return cls(v >> 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", bn)))
class Type3Tag(nfc.tag.Tag):
"""Implementation of the NFC Forum Type 3 Tag specification.
The NFC Forum Type 3 Tag is based on the Sony FeliCa protocol and
command specification. An NFC Forum compliant Type 3 Tag responds
to a FeliCa polling command with system code 0x12FC and was
configured to support service code 0x000B for NDEF data read and
service code 0x0009 for NDEF data write (the latter may not be
present if the tag is read-only) without encryption.
"""
TYPE = "Type3Tag"
class NDEF(nfc.tag.Tag.NDEF):
# Type 3 Tag specific implementation of the NDEF access type
# class that is returned by the Tag.ndef attribute.
def _read_attribute_data(self):
try:
data = self._tag.read_from_ndef_service(0)
except Type3TagCommandError:
return None
if sum(data[0:14]) != unpack(">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)

987
src/lib/nfc/tag/tt3_sony.py Normal file
View File

@ -0,0 +1,987 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2014, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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("<H", data[i:i+2])[0] for i in range(1, len(data), 2)]
def request_response(self):
"""Verify that a card is still present and get its operating mode.
The Request Response command returns the current operating
state of the card. The operating state changes with the
authentication process, a card is in Mode 0 after power-up or
a Polling command, transitions to Mode 1 with Authentication1,
to Mode 2 with Authentication2, and Mode 3 with any of the
card issuance commands. The :meth:`request_response` method
returns the mode as an integer.
Command execution errors raise
:exc:`~nfc.tag.TagCommandError`.
"""
a, b, e = self.pmm[3] & 7, self.pmm[3] >> 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("<H", service_index)
data = self.send_cmd_recv_rsp(0x0A, data, timeout, check_status=False)
if data != b"\xFF\xFF":
unpack_format = "<H" if len(data) == 2 else "<HH"
return unpack(unpack_format, data)
def request_system_code(self):
"""Return all system codes that are registered in the card.
A card has one or more system codes that correspond to logical
partitions (systems). Each system has a system code that could
be used in a polling command to activate that system. The
system codes responded by the card are returned as a list of
16-bit integers. ::
for system_code in tag.request_system_code():
print("System {0:04X}".format(system_code))
Command execution errors raise :exc:`~nfc.tag.TagCommandError`.
"""
log.debug("request system code list")
a, e = self.pmm[3] & 7, self.pmm[3] >> 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", 0x7FFF ^ (2**14 - 2**protect_from))
if protect_from == 0 and self.ndef is not None:
attribute_data = self.read_without_mac(0)
attribute_data[10] = 0x00
attribute_data[14:16] = 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("<H", mc[0:2])[0]
for nmaxb in range(14):
if rw_bits >> (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", mc[0:2])[0]
self._writeable = bool(rw_bits & 0x3ff == 0x3ff)
return attributes
def __init__(self, clf, target):
super(FelicaLiteS, self).__init__(clf, target)
self._product = self.IC_CODE_MAP[self.pmm[1]]
def dump(self):
def oprint(octets):
return ' '.join(['%02x' % x for x in octets])
lines = super(FelicaLiteS, self).dump()
text = ("WCNT[3]", "MAC_A[8]", "STATE")
config = dict(zip(range(0x90, 0x90+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
def protect(self, password=None, read_protect=False, protect_from=0):
"""Protect a FeliCa Lite-S Tag.
A FeliCa Lite-S Tag can be write and read protected with a
custom password (or the default manufacturer key if the
password is an empty string or bytearray). Note that the
*read_protect* flag is only evaluated when a *password* is
provided.
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")
# 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: # system block protected
if mc[5] & 1 == 0: # key change disabled
log.info("card key can not be changed")
return False
if self._authenticated is False:
log.info("authentication required to change key")
return False
# if password is empty use factory key of 16 zero bytes
key = password[0:16].encode("ascii") if password else b'\0' * 16
log.debug("protect with key %s", hexlify(key).decode())
ckv = self.read_without_mac(0x86)
ckv = min(unpack("<H", ckv[0:2])[0] + 1, 0xffff)
log.debug("new card key version is {0}".format(ckv))
self.write_without_mac(pack("<H", ckv) + b"\0" * 14, 0x86)
self.write_without_mac(key[7::-1] + key[15:7:-1], 0x87)
if not self.authenticate(key):
log.error("failed to authenticate with new card key")
return False
if read_protect and protect_from < 14:
log.debug("read protect blocks {0}--13".format(protect_from))
protect_mask = pack("<H", 2**14 - 2**protect_from)
mc[6:8] = protect_mask
if protect_from < 14:
log.debug("write protect blocks {0}--13".format(protect_from))
protect_mask = pack("<H", 2**14 - 2**protect_from)
mc[8:10] = mc[10:12] = protect_mask
if protect_from == 0 and self.ndef is not None:
attribute_data = self.read_without_mac(0)
attribute_data[10] = 0x00
attribute_data[14:16] = 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
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]]

579
src/lib/nfc/tag/tt4.py Normal file
View File

@ -0,0 +1,579 @@
# -*- coding: latin-1 -*-
# -----------------------------------------------------------------------------
# Copyright 2012, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
#
# 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)

View File

@ -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}",

View File

@ -75,7 +75,7 @@ class Barcode_Recipe_Selection(Test_Test):
else:
lines = data.splitlines()
#lines = data.split("-")
candidates = [i for i in lines if len(i) in(10,12)]
candidates = [i for i in lines if len(i)in (9,10,12)]
if len(candidates)>0:
# RECIPE CODE FOUND
self.recipe=candidates[-1]

View File

@ -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,
import_recipes(
config=self.config,
csv_path=csv_path,
defaults=defaults,
unsupported_steps=self.unsupported_steps,
logger=self.log,
)
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()
# 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,
export_recipes(
config=self.config,
csv_path=csv_path,
logger=self.log,
)
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,
}
# 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)