Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
755ce179b0
70
config/instruction_images/st-ten-1/000825276.svg
Normal file
70
config/instruction_images/st-ten-1/000825276.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.6 MiB |
49
config/instruction_images/st-ten-1/DEFAULT.svg
Normal file
49
config/instruction_images/st-ten-1/DEFAULT.svg
Normal 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 |
BIN
config/instruction_images/st-ten-1/img/arw-yel-down.png
Normal file
BIN
config/instruction_images/st-ten-1/img/arw-yel-down.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
BIN
config/instruction_images/st-ten-1/img/ok.png
Normal file
BIN
config/instruction_images/st-ten-1/img/ok.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
config/instruction_images/st-ten-1/img/tape_black.png
Normal file
BIN
config/instruction_images/st-ten-1/img/tape_black.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
config/instruction_images/st-ten-1/img/tape_white.png
Normal file
BIN
config/instruction_images/st-ten-1/img/tape_white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
|
|
@ -14,10 +14,11 @@ remote_api: absent
|
||||||
tecna_t3: present
|
tecna_t3: present
|
||||||
vision_saver: absent
|
vision_saver: absent
|
||||||
vision: absent
|
vision: absent
|
||||||
screwdriver: absent
|
screwdriver: present
|
||||||
#fixture_id: present
|
fixture_id: present
|
||||||
digital_io: present
|
digital_io: present
|
||||||
external_flush_blow: absent
|
external_flush_blow: absent
|
||||||
|
barcode_recipe_selection: present
|
||||||
|
|
||||||
# VERO PROJECT LOCAL SERVER
|
# VERO PROJECT LOCAL SERVER
|
||||||
[archive_synchronizer_extra]
|
[archive_synchronizer_extra]
|
||||||
|
|
@ -104,9 +105,9 @@ settling_pressure_min_percent: 5
|
||||||
settling_pressure_max_percent: 5
|
settling_pressure_max_percent: 5
|
||||||
test_pressure: 7000
|
test_pressure: 7000
|
||||||
test_time: 10
|
test_time: 10
|
||||||
test_pressure_qpos: 10 #Q+ Upper test leak limit
|
test_pressure_qpos: 5 #Q+ Upper test leak limit
|
||||||
test_pressure_qneg: 30 #Q- Lower test leak limit
|
test_pressure_qneg: 17 #Q- Lower test leak limit
|
||||||
test_pressure_tt_qpos: 1 # Q+ Upper test leak limit (tube-tube)
|
test_pressure_tt_qpos: 5 # Q+ Upper test leak limit (tube-tube)
|
||||||
test_pressure_tt_qneg: 5 # Q- Lower test leak limit (tube-tube)
|
test_pressure_tt_qneg: 5 # Q- Lower test leak limit (tube-tube)
|
||||||
flush_time: 1
|
flush_time: 1
|
||||||
flush_pressure: 100
|
flush_pressure: 100
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ from PyQt5.QtCore import QThread
|
||||||
from requests.adapters import HTTPAdapter, Retry
|
from requests.adapters import HTTPAdapter, Retry
|
||||||
from urllib3.exceptions import InsecureRequestWarning
|
from urllib3.exceptions import InsecureRequestWarning
|
||||||
|
|
||||||
|
from lib.helpers.recipe_manager import import_recipes, backup_current_recipes
|
||||||
|
|
||||||
from .component import Component
|
from .component import Component
|
||||||
from ui.helpers import get_main_window
|
from ui.helpers import get_main_window
|
||||||
# Suppress insecure request warning
|
# Suppress insecure request warning
|
||||||
|
|
@ -135,6 +137,9 @@ class ArchiveSynchronizer(Component):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def parse_response_and_execute(self, response):
|
def parse_response_and_execute(self, response):
|
||||||
|
"""
|
||||||
|
Parse the response and execute actions based on the `ACTIONS_TO_DO` received.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
|
|
@ -147,11 +152,53 @@ class ArchiveSynchronizer(Component):
|
||||||
actions = [actions]
|
actions = [actions]
|
||||||
|
|
||||||
for action in actions:
|
for action in actions:
|
||||||
remote_path = action.get("remote_path")
|
action_type = action.get("action") # Determine which type of action to perform
|
||||||
local_path = action.get("local_path")
|
|
||||||
self.log.info(f"Executing remote fetch with remote_path: {remote_path} and local_path: {local_path}")
|
if action_type == "import": # Handle import action
|
||||||
result = self.remote_fetch(remote_path=remote_path, local_path=local_path)
|
remote_path = action.get("remote_path")
|
||||||
self.log.info(f"Remote fetch result: {result}")
|
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:
|
except json.JSONDecodeError:
|
||||||
self.log.error("Failed to decode JSON response")
|
self.log.error("Failed to decode JSON response")
|
||||||
|
|
@ -283,8 +330,10 @@ class ArchiveSynchronizer(Component):
|
||||||
self.log_to_db(log_time, log_info_type, log_info)
|
self.log_to_db(log_time, log_info_type, log_info)
|
||||||
return {"error": "Unexpected HTTP response status", "last_update_info": last_update_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)
|
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))
|
local_file_path = os.path.join(local_path, os.path.basename(remote_path))
|
||||||
with open(local_file_path, "wb") as f:
|
with open(local_file_path, "wb") as f:
|
||||||
f.write(response.content)
|
f.write(response.content)
|
||||||
|
|
@ -292,6 +341,7 @@ class ArchiveSynchronizer(Component):
|
||||||
log_info += f" - File downloaded successfully: {local_file_path}"
|
log_info += f" - File downloaded successfully: {local_file_path}"
|
||||||
self.log.info(log_info)
|
self.log.info(log_info)
|
||||||
self.log_to_db(log_time, log_info_type, 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}
|
return {"downloaded_file": local_file_path, "last_update_info": last_update_info}
|
||||||
|
|
||||||
except requests.ConnectionError as e:
|
except requests.ConnectionError as e:
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import platform
|
||||||
from PyQt5.QtCore import QMutex, Qt, QTimer, pyqtSlot, pyqtSignal
|
from PyQt5.QtCore import QMutex, Qt, QTimer, pyqtSlot, pyqtSignal
|
||||||
from .component import Component
|
from .component import Component
|
||||||
import ndef
|
import ndef
|
||||||
import nfc
|
import src.lib.nfc
|
||||||
from nfc.clf import RemoteTarget
|
from src.lib.nfc.clf import RemoteTarget
|
||||||
|
|
||||||
|
|
||||||
class RFID_PN532(Component):
|
class RFID_PN532(Component):
|
||||||
|
|
@ -26,7 +26,7 @@ class RFID_PN532(Component):
|
||||||
self._period = 1
|
self._period = 1
|
||||||
|
|
||||||
def open_device(self):
|
def open_device(self):
|
||||||
self.clf = nfc.ContactlessFrontend()
|
self.clf = src.lib.nfc.ContactlessFrontend()
|
||||||
for dev in self.dev_list:
|
for dev in self.dev_list:
|
||||||
self.connected = self.clf.open(dev)
|
self.connected = self.clf.open(dev)
|
||||||
if self.connected:
|
if self.connected:
|
||||||
|
|
@ -52,7 +52,7 @@ class RFID_PN532(Component):
|
||||||
else:
|
else:
|
||||||
target = self.clf.sense(RemoteTarget('106A'), RemoteTarget('106B'), RemoteTarget('212F'))
|
target = self.clf.sense(RemoteTarget('106A'), RemoteTarget('106B'), RemoteTarget('212F'))
|
||||||
if target is not None:
|
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:
|
if tag is not None:
|
||||||
self.log.debug("tag present")
|
self.log.debug("tag present")
|
||||||
if tag.ndef is not None:
|
if tag.ndef is not None:
|
||||||
|
|
|
||||||
1
src/lib/__init__.py
Normal file
1
src/lib/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
405
src/lib/helpers/recipe_manager.py
Normal file
405
src/lib/helpers/recipe_manager.py
Normal 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
47
src/lib/nfc/__init__.py
Normal 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
214
src/lib/nfc/__main__.py
Normal 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
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
242
src/lib/nfc/clf/acr122.py
Normal 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
105
src/lib/nfc/clf/arygon.py
Normal 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
660
src/lib/nfc/clf/device.py
Normal 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
316
src/lib/nfc/clf/pn531.py
Normal 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
454
src/lib/nfc/clf/pn532.py
Normal 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
399
src/lib/nfc/clf/pn533.py
Normal 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
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
986
src/lib/nfc/clf/rcs380.py
Normal 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
376
src/lib/nfc/clf/rcs956.py
Normal 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
|
||||||
345
src/lib/nfc/clf/transport.py
Normal file
345
src/lib/nfc/clf/transport.py
Normal 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
577
src/lib/nfc/clf/udp.py
Normal 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
895
src/lib/nfc/dep.py
Normal 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'
|
||||||
29
src/lib/nfc/handover/__init__.py
Normal file
29
src/lib/nfc/handover/__init__.py
Normal 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
|
||||||
118
src/lib/nfc/handover/client.py
Normal file
118
src/lib/nfc/handover/client.py
Normal 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()
|
||||||
128
src/lib/nfc/handover/server.py
Normal file
128
src/lib/nfc/handover/server.py
Normal 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')]
|
||||||
38
src/lib/nfc/llcp/__init__.py
Normal file
38
src/lib/nfc/llcp/__init__.py
Normal 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
42
src/lib/nfc/llcp/err.py
Normal 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
886
src/lib/nfc/llcp/llc.py
Normal 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
945
src/lib/nfc/llcp/pdu.py
Normal 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
542
src/lib/nfc/llcp/sec.py
Normal 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
177
src/lib/nfc/llcp/socket.py
Normal 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
733
src/lib/nfc/llcp/tco.py
Normal 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
|
||||||
36
src/lib/nfc/snep/__init__.py
Normal file
36
src/lib/nfc/snep/__init__.py
Normal 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
247
src/lib/nfc/snep/client.py
Normal 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
175
src/lib/nfc/snep/server.py
Normal 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
480
src/lib/nfc/tag/__init__.py
Normal 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
555
src/lib/nfc/tag/tt1.py
Normal 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)
|
||||||
159
src/lib/nfc/tag/tt1_broadcom.py
Normal file
159
src/lib/nfc/tag/tt1_broadcom.py
Normal 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
697
src/lib/nfc/tag/tt2.py
Normal 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
771
src/lib/nfc/tag/tt2_nxp.py
Normal 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
930
src/lib/nfc/tag/tt3.py
Normal 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
987
src/lib/nfc/tag/tt3_sony.py
Normal 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
579
src/lib/nfc/tag/tt4.py
Normal 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)
|
||||||
|
|
@ -8,8 +8,8 @@ from pathlib import Path
|
||||||
|
|
||||||
import serial
|
import serial
|
||||||
import ndef
|
import ndef
|
||||||
import nfc
|
from src.lib import nfc as nfc
|
||||||
from nfc.clf import RemoteTarget
|
from src.lib.nfc.clf import RemoteTarget
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format="{asctime}:{name}:{levelname}:{message}",
|
format="{asctime}:{name}:{levelname}:{message}",
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ class Barcode_Recipe_Selection(Test_Test):
|
||||||
else:
|
else:
|
||||||
lines = data.splitlines()
|
lines = data.splitlines()
|
||||||
#lines = data.split("-")
|
#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:
|
if len(candidates)>0:
|
||||||
# RECIPE CODE FOUND
|
# RECIPE CODE FOUND
|
||||||
self.recipe=candidates[-1]
|
self.recipe=candidates[-1]
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ from PyQt5.QtCore import QTimer, pyqtSignal
|
||||||
from PyQt5.QtGui import QKeySequence
|
from PyQt5.QtGui import QKeySequence
|
||||||
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QShortcut
|
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QShortcut
|
||||||
import shutil
|
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.crud import Crud, Json_External_Dialog_Editor_Cell_Widget
|
||||||
from ui.helpers import replace_widget
|
from ui.helpers import replace_widget
|
||||||
from ui.recipe_spec_and_step_editor import Recipe_Spec_And_Step_Editor
|
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
|
# IMPORT RECIPES FROM CSV FILE TO DATABASE
|
||||||
def import_recipes(self, csv_path=None, defaults=None):
|
def import_recipes(self, csv_path=None, defaults=None):
|
||||||
if defaults is None:
|
import_recipes(
|
||||||
global noner
|
config=self.config,
|
||||||
defaults = self.config.get("recipes_defaults", noner)
|
csv_path=csv_path,
|
||||||
if csv_path is None:
|
defaults=defaults,
|
||||||
options = QFileDialog.Options()
|
unsupported_steps=self.unsupported_steps,
|
||||||
options |= QFileDialog.DontUseNativeDialog
|
logger=self.log,
|
||||||
csv_path, _ = QFileDialog.getOpenFileName(
|
)
|
||||||
self,
|
|
||||||
"Importazione ricette",
|
|
||||||
"ricette.csv",
|
|
||||||
"CSV data (*.csv);;All Files (*)",
|
|
||||||
options=options,
|
|
||||||
)
|
|
||||||
csv_path = str(csv_path)
|
|
||||||
if not len(csv_path):
|
|
||||||
return
|
|
||||||
self.log.info(f"recipes: importing recipes from {csv_path}")
|
|
||||||
recipe_name_field = self.config.get("recipe", {}).get("recipe_name_field", "codice_ricetta").strip()
|
|
||||||
part_number_field = self.config.get("recipe", {}).get("part_number_field", "part_number").strip()
|
|
||||||
description_field = self.config.get("recipe", {}).get("description_field", "descrizione").strip()
|
|
||||||
barcode_enable_field = self.config.get("recipe", {}).get("barcode_enable_field", "verifica_codice_a_barre_abilitata").strip()
|
|
||||||
|
|
||||||
with open(csv_path, "r", encoding="utf-8-sig") as f:
|
|
||||||
reader = csv.DictReader(f)
|
|
||||||
count = 0
|
|
||||||
for ucrow in reader:
|
|
||||||
row = dict((k.lower(), v) for k, v in ucrow.items())
|
|
||||||
recipe_name = row.get(recipe_name_field, defaults["codice_ricetta"])
|
|
||||||
steps_specs = self.read_steps(row, defaults=defaults)
|
|
||||||
|
|
||||||
# create recipe or update existing one in DB
|
|
||||||
try:
|
|
||||||
recipe = Recipes.get_by_id(recipe_name)
|
|
||||||
steps = recipe.get_steps_map()
|
|
||||||
recipe_is_new = False
|
|
||||||
except Recipes.DoesNotExist:
|
|
||||||
recipe = Recipes(name=recipe_name, part_number="TEMPORARY")
|
|
||||||
steps = {}
|
|
||||||
for step_name, step_spec in steps_specs.items():
|
|
||||||
if step_name not in self.unsupported_steps:
|
|
||||||
steps[step_name] = step_spec
|
|
||||||
recipe_is_new = True
|
|
||||||
recipe.client = row.get("cliente", defaults["cliente"])
|
|
||||||
recipe.part_number = row.get(part_number_field, defaults["part_number"])
|
|
||||||
recipe.description = row.get(description_field, defaults["descrizione"])
|
|
||||||
recipe.spec = {
|
|
||||||
"count": len(row.get("dimensione_lotto_abilitata", defaults["dimensione_lotto_abilitata"])) and "count" not in self.unsupported_steps,
|
|
||||||
"connector": len(
|
|
||||||
row.get("verifica_connettore_abilitata", defaults["verifica_connettore_abilitata"])) and "connector" not in self.unsupported_steps,
|
|
||||||
"barcodes": len(row.get(barcode_enable_field, defaults["verifica_codice_a_barre_abilitata"])) and "barcodes" not in self.unsupported_steps,
|
|
||||||
"resistance": len(row.get("verifica_resistenza_connettore_abilitata",
|
|
||||||
defaults["verifica_resistenza_connettore_abilitata"])) and "resistance" not in self.unsupported_steps,
|
|
||||||
"screws": len(row.get("avvitatura_abilitata", defaults["avvitatura_abilitata"])) and "screws" not in self.unsupported_steps,
|
|
||||||
"instruction": len(row.get("istruzione_abilitata", defaults["istruzione_abilitata"])) and "instruction" not in self.unsupported_steps,
|
|
||||||
"instruction_extra": len(row.get("istruzione_abilitata_extra", defaults["istruzione_abilitata_extra"])) and "instruction_extra" not in self.unsupported_steps,
|
|
||||||
"leak_1": len(row.get("prova_tenuta_abilitata", defaults["prova_tenuta_abilitata"])) and "leak_1" not in self.unsupported_steps,
|
|
||||||
"leak_2": len(row.get("prova_tenuta_abilitata_2", defaults["prova_tenuta_abilitata_2"])) and "leak_2" not in self.unsupported_steps,
|
|
||||||
"vision": len(row.get("test_visione_abilitato", defaults["test_visione_abilitato"])) and "vision" not in self.unsupported_steps,
|
|
||||||
"print": len(row.get("stampa_etichetta_abilitata", defaults["stampa_etichetta_abilitata"])) and "print" not in self.unsupported_steps,
|
|
||||||
"steps": steps,
|
|
||||||
}
|
|
||||||
recipe.spec["steps"]=steps_specs
|
|
||||||
if recipe_is_new:
|
|
||||||
recipe.save(force_insert=True)
|
|
||||||
else:
|
|
||||||
recipe.save()
|
|
||||||
count += 1
|
|
||||||
db.commit()
|
|
||||||
self.log.info(f"recipes: imported {count} rows.")
|
|
||||||
self.crud.refresh()
|
|
||||||
|
|
||||||
# EXPORT RECIPES TABLE TO CSV FILE
|
# EXPORT RECIPES TABLE TO CSV FILE
|
||||||
def export_recipes(self, csv_path=None):
|
def export_recipes(self, csv_path=None):
|
||||||
if csv_path is None:
|
export_recipes(
|
||||||
options = QFileDialog.Options()
|
config=self.config,
|
||||||
options |= QFileDialog.DontUseNativeDialog
|
csv_path=csv_path,
|
||||||
csv_path, _ = QFileDialog.getSaveFileName(
|
logger=self.log,
|
||||||
self,
|
)
|
||||||
"Esportazione ricette",
|
|
||||||
"ricette.csv",
|
|
||||||
"CSV data (*.csv);;All Files (*)",
|
|
||||||
options=options,
|
|
||||||
)
|
|
||||||
csv_path = str(csv_path)
|
|
||||||
if not len(csv_path):
|
|
||||||
return
|
|
||||||
if not csv_path.lower().endswith(".csv"):
|
|
||||||
csv_path += ".csv"
|
|
||||||
csv_dir = os.path.dirname(csv_path)
|
|
||||||
if len(csv_dir):
|
|
||||||
os.makedirs(csv_dir, exist_ok=True)
|
|
||||||
recipe_name_field = self.config.get("recipe", {}).get("recipe_name_field", "codice_ricetta").strip()
|
|
||||||
barcode_enable_field = self.config.get("recipe", {}).get("barcode_enable_field", "verifica_codice_a_barre_abilitata").strip()
|
|
||||||
barcode_serial_field = self.config.get("recipe", {}).get("barcode_serial_field", "codice_a_barre").strip()
|
|
||||||
print_template_field = self.config.get("recipe", {}).get("label_template_field", "modello_etichetta").strip()
|
|
||||||
data = []
|
|
||||||
fieldnames = set() # Use a set to avoid duplicates
|
|
||||||
for recipe in list(Recipes.select()):
|
|
||||||
steps = recipe.get_steps_map()
|
|
||||||
exportable = {
|
|
||||||
# BASE SECTION
|
|
||||||
recipe_name_field: recipe.name,
|
|
||||||
"cliente": recipe.client,
|
|
||||||
"part_number": recipe.part_number,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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):
|
def delete_recipes(self):
|
||||||
ret = QMessageBox.warning(
|
ret = QMessageBox.warning(
|
||||||
None,
|
None,
|
||||||
|
|
@ -507,40 +304,6 @@ class Recipe_Selection(Widget):
|
||||||
if ret == QMessageBox.Ok:
|
if ret == QMessageBox.Ok:
|
||||||
Recipes.delete().execute()
|
Recipes.delete().execute()
|
||||||
self.crud.refresh()
|
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)
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user