From 270366af324dda75dce5f649fe7561b8300b273b Mon Sep 17 00:00:00 2001 From: neo-nb3 Date: Sun, 22 Dec 2024 18:39:13 +0100 Subject: [PATCH] fork nfcpy package as local lib, fixed pn532 baudrate, tbt --- src/lib/__init__.py | 1 + src/lib/nfc/__init__.py | 47 ++ src/lib/nfc/__main__.py | 214 +++++ src/lib/nfc/clf/__init__.py | 1251 ++++++++++++++++++++++++++++++ src/lib/nfc/clf/acr122.py | 242 ++++++ src/lib/nfc/clf/arygon.py | 105 +++ src/lib/nfc/clf/device.py | 660 ++++++++++++++++ src/lib/nfc/clf/pn531.py | 316 ++++++++ src/lib/nfc/clf/pn532.py | 454 +++++++++++ src/lib/nfc/clf/pn533.py | 399 ++++++++++ src/lib/nfc/clf/pn53x.py | 1064 +++++++++++++++++++++++++ src/lib/nfc/clf/rcs380.py | 986 +++++++++++++++++++++++ src/lib/nfc/clf/rcs956.py | 376 +++++++++ src/lib/nfc/clf/transport.py | 345 ++++++++ src/lib/nfc/clf/udp.py | 577 ++++++++++++++ src/lib/nfc/dep.py | 895 +++++++++++++++++++++ src/lib/nfc/handover/__init__.py | 29 + src/lib/nfc/handover/client.py | 118 +++ src/lib/nfc/handover/server.py | 128 +++ src/lib/nfc/llcp/__init__.py | 38 + src/lib/nfc/llcp/err.py | 42 + src/lib/nfc/llcp/llc.py | 886 +++++++++++++++++++++ src/lib/nfc/llcp/pdu.py | 945 ++++++++++++++++++++++ src/lib/nfc/llcp/sec.py | 542 +++++++++++++ src/lib/nfc/llcp/socket.py | 177 +++++ src/lib/nfc/llcp/tco.py | 733 +++++++++++++++++ src/lib/nfc/snep/__init__.py | 36 + src/lib/nfc/snep/client.py | 247 ++++++ src/lib/nfc/snep/server.py | 175 +++++ src/lib/nfc/tag/__init__.py | 480 ++++++++++++ src/lib/nfc/tag/tt1.py | 555 +++++++++++++ src/lib/nfc/tag/tt1_broadcom.py | 159 ++++ src/lib/nfc/tag/tt2.py | 697 +++++++++++++++++ src/lib/nfc/tag/tt2_nxp.py | 771 ++++++++++++++++++ src/lib/nfc/tag/tt3.py | 930 ++++++++++++++++++++++ src/lib/nfc/tag/tt3_sony.py | 987 +++++++++++++++++++++++ src/lib/nfc/tag/tt4.py | 579 ++++++++++++++ src/test/rfid.py | 4 +- 38 files changed, 17188 insertions(+), 2 deletions(-) create mode 100644 src/lib/__init__.py create mode 100644 src/lib/nfc/__init__.py create mode 100644 src/lib/nfc/__main__.py create mode 100644 src/lib/nfc/clf/__init__.py create mode 100644 src/lib/nfc/clf/acr122.py create mode 100644 src/lib/nfc/clf/arygon.py create mode 100644 src/lib/nfc/clf/device.py create mode 100644 src/lib/nfc/clf/pn531.py create mode 100644 src/lib/nfc/clf/pn532.py create mode 100644 src/lib/nfc/clf/pn533.py create mode 100644 src/lib/nfc/clf/pn53x.py create mode 100644 src/lib/nfc/clf/rcs380.py create mode 100644 src/lib/nfc/clf/rcs956.py create mode 100644 src/lib/nfc/clf/transport.py create mode 100644 src/lib/nfc/clf/udp.py create mode 100644 src/lib/nfc/dep.py create mode 100644 src/lib/nfc/handover/__init__.py create mode 100644 src/lib/nfc/handover/client.py create mode 100644 src/lib/nfc/handover/server.py create mode 100644 src/lib/nfc/llcp/__init__.py create mode 100644 src/lib/nfc/llcp/err.py create mode 100644 src/lib/nfc/llcp/llc.py create mode 100644 src/lib/nfc/llcp/pdu.py create mode 100644 src/lib/nfc/llcp/sec.py create mode 100644 src/lib/nfc/llcp/socket.py create mode 100644 src/lib/nfc/llcp/tco.py create mode 100644 src/lib/nfc/snep/__init__.py create mode 100644 src/lib/nfc/snep/client.py create mode 100644 src/lib/nfc/snep/server.py create mode 100644 src/lib/nfc/tag/__init__.py create mode 100644 src/lib/nfc/tag/tt1.py create mode 100644 src/lib/nfc/tag/tt1_broadcom.py create mode 100644 src/lib/nfc/tag/tt2.py create mode 100644 src/lib/nfc/tag/tt2_nxp.py create mode 100644 src/lib/nfc/tag/tt3.py create mode 100644 src/lib/nfc/tag/tt3_sony.py create mode 100644 src/lib/nfc/tag/tt4.py diff --git a/src/lib/__init__.py b/src/lib/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/lib/__init__.py @@ -0,0 +1 @@ + diff --git a/src/lib/nfc/__init__.py b/src/lib/nfc/__init__.py new file mode 100644 index 0000000..6d0c47d --- /dev/null +++ b/src/lib/nfc/__init__.py @@ -0,0 +1,47 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +from . import clf # noqa: F401 +from . import tag # noqa: F401 +from . import llcp # noqa: F401 +from . import snep # noqa: F401 +from . import handover # noqa: F401 +from .clf import ContactlessFrontend # noqa: F401 + +import logging +logging.getLogger(__name__).addHandler(logging.NullHandler()) +logging.getLogger(__name__).setLevel(logging.INFO) + +# METADATA #################################################################### + +__version__ = "1.0.4" + +__title__ = "nfcpy" +__description__ = "Python module for Near Field Communication." +__uri__ = "https://github.com/nfcpy/nfcpy" + +__author__ = "Stephen Tiedemann" +__email__ = "stephen.tiedemann@gmail.com" + +__license__ = "EUPL" +__copyright__ = "Copyright (c) 2009, 2019 Stephen Tiedemann" + +############################################################################### diff --git a/src/lib/nfc/__main__.py b/src/lib/nfc/__main__.py new file mode 100644 index 0000000..a5bc5a0 --- /dev/null +++ b/src/lib/nfc/__main__.py @@ -0,0 +1,214 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2016 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import nfc +import nfc.clf.device +import nfc.clf.transport + +import os +import errno +import logging +import platform +import argparse +import subprocess + +description = """ + +The nfcpy module implements a near field communication software stack +for reading and writing NFC Tags or peer-to-peer communication with +another NFC Device. It requires an NFC radio module connected through +either USB or serial interface. The nfcpy module is supposed to be +used within other applications, executing it as a module will try to +locate contactless devices connected to this machine. + +""" + + +def main(args): + print("This is the %s version of nfcpy run in Python %s\non %s" % + (nfc.__version__, platform.python_version(), platform.platform())) + print("I'm now searching your system for contactless devices") + + logging.basicConfig() + log_levels = (logging.WARN, logging.INFO, logging.DEBUG, logging.DEBUG-1) + log_level = log_levels[min(args.verbose, len(log_levels) - 1)] + logging.getLogger('nfc').setLevel(log_level) + + found = 0 + for vid, pid, bus, dev in nfc.clf.transport.USB.find("usb"): + if (vid, pid) in nfc.clf.device.usb_device_map: + path = "usb:{0:03d}:{1:03d}".format(bus, dev) + try: + clf = nfc.ContactlessFrontend(path) + print("** found %s" % clf.device) + clf.close() + found += 1 + except IOError as error: + if error.errno == errno.EACCES: + usb_device_access_denied(bus, dev, vid, pid, path) + elif error.errno == errno.EBUSY: + usb_device_found_is_busy(bus, dev, vid, pid, path) + + if args.search_tty: + for dev in nfc.clf.transport.TTY.find("tty")[0]: + path = "tty:{0}".format(dev[8:]) + try: + clf = nfc.ContactlessFrontend(path) + print("** found %s" % clf.device) + clf.close() + found += 1 + except IOError as error: + if error.errno == errno.EACCES: + print("access denied for device with path %s" % path) + elif error.errno == errno.EBUSY: + print("the device with path %s is busy" % path) + else: + print("I'm not trying serial devices because you haven't told me") + print("-- add the option '--search-tty' to have me looking") + print("-- but beware that this may break other serial devs") + + if not found: + print("Sorry, but I couldn't find any contactless device") + + +def usb_device_access_denied(bus, dev, vid, pid, path): + info = "** found usb:{vid:04x}:{pid:04x} at {path} but access is denied" + print(info.format(vid=vid, pid=pid, path=path)) + if platform.system().lower() == "linux": + devnode = "/dev/bus/usb/{0:03d}/{1:03d}".format(bus, dev) + if not os.access(devnode, os.R_OK | os.W_OK): + import pwd + import grp + usrname = pwd.getpwuid(os.getuid()).pw_name + devinfo = os.stat(devnode) + dev_usr = pwd.getpwuid(devinfo.st_uid).pw_name + dev_grp = grp.getgrgid(devinfo.st_gid).gr_name + try: + plugdev = grp.getgrnam("plugdev") + except KeyError: + plugdev = None + + udev_rule = 'SUBSYSTEM==\\"usb\\", ACTION==\\"add\\", ' \ + 'ATTRS{{idVendor}}==\\"{vid:04x}\\", ' \ + 'ATTRS{{idProduct}}==\\"{pid:04x}\\", ' \ + '{action}' + udev_file = "/etc/udev/rules.d/nfcdev.rules" + + print("-- the device is owned by '{dev_usr}' but you are '{user}'" + .format(dev_usr=dev_usr, user=usrname)) + print("-- also members of the '{dev_grp}' group would be permitted" + .format(dev_grp=dev_grp)) + print("-- you could use 'sudo' but this is not recommended") + + if plugdev is None: + print("-- it's better to adjust the device permissions") + action = 'MODE=\\"0666\\"' + udev_rule = udev_rule.format(vid=vid, pid=pid, action=action) + print(" sudo sh -c 'echo {udev_rule} >> {udev_file}'" + .format(udev_rule=udev_rule, udev_file=udev_file)) + print(" sudo udevadm control -R # then re-attach device") + elif dev_grp != "plugdev": + print("-- better assign the device to the 'plugdev' group") + action = 'GROUP=\\"plugdev\\"' + udev_rule = udev_rule.format(vid=vid, pid=pid, action=action) + print(" sudo sh -c 'echo {udev_rule} >> {udev_file}'" + .format(udev_rule=udev_rule, udev_file=udev_file)) + print(" sudo udevadm control -R # then re-attach device") + if usrname not in plugdev.gr_mem: + print("-- and make yourself member of the 'plugdev' group") + print(" sudo adduser {0} plugdev".format(usrname)) + print(" su - {0} # or logout once".format(usrname)) + elif usrname not in plugdev.gr_mem: + print("-- you should add yourself to the 'plugdev' group") + print(" sudo adduser {0} plugdev".format(usrname)) + print(" su - {0} # or logout once".format(usrname)) + else: + print("-- but unfortunately I have no better idea than that") + + +def usb_device_found_is_busy(bus, dev, vid, pid, path): + info = "** found usb:{vid:04x}:{pid:04x} at {path} but it's already used" + print(info.format(vid=vid, pid=pid, path=path)) + if platform.system().lower() == "linux": + sysfs = '/sys/bus/usb/devices/' + for entry in os.listdir(sysfs): + if not entry.startswith("usb") and ':' not in entry: + sysfs_device_entry = sysfs + entry + '/' + busnum = open(sysfs_device_entry + 'busnum').read().strip() + devnum = open(sysfs_device_entry + 'devnum').read().strip() + if int(busnum) == bus and int(devnum) == dev: + break + else: + print("-- impossible but nothing found in /sys/bus/usb/devices") + return + + # We now have the sysfs entry for the device in question. All + # supported contactless devices have a single configuration + # that will be listed if the device is used by another driver. + + blf = "/etc/modprobe.d/blacklist-nfc.conf" + sysfs_config_entry = sysfs_device_entry[:-1] + ":1.0/" + print("-- scan sysfs entry at '%s'" % sysfs_config_entry) + driver = os.readlink(sysfs_config_entry + "driver").split('/')[-1] + print("-- the device is used by the '%s' kernel driver" % driver) + if os.access(sysfs_config_entry + "nfc", os.F_OK): + print("-- this kernel driver belongs to the linux nfc subsystem") + print("-- you can remove it to free the device for this session") + print(" sudo modprobe -r %s" % driver) + print("-- and blacklist the driver to prevent loading next time") + print(" sudo sh -c 'echo blacklist %s >> %s'" % (driver, blf)) + elif driver == "usbfs": + print("-- this indicates a user mode driver with libusb") + devnode = "/dev/bus/usb/{0:03d}/{1:03d}".format(bus, dev) + print("-- find the process that uses " + devnode) + try: + subprocess.check_output("which lsof".split()) + except subprocess.CalledProcessError: + print("-- there is no 'lsof' command, can't help further") + else: + lsof = "lsof -t " + devnode + try: + pid = subprocess.check_output(lsof.split()).strip() + except subprocess.CalledProcessError: + pid = None + if pid is not None: + ps = "ps --no-headers -o cmd -p %s" % pid + cmd = subprocess.check_output(ps.split()).strip() + cwd = os.readlink("/proc/%s/cwd" % pid) + print("-- found that process %s uses the device" % pid) + print("-- process %s is '%s'" % (pid, cmd)) + print("-- in directory '%s'" % cwd) + else: + print(" ps --no-headers -o cmd -p `sudo %s`" % lsof) + + +parser = argparse.ArgumentParser( + prog="python -m nfc", description=description) + +parser.add_argument( + "--search-tty", action="store_true", + help="do also search for serial devices on linux") + +parser.add_argument( + "--verbose", "-v", action="count", default=0, + help="be verbose. Multiple -v options increase the verbosity.") + +main(parser.parse_args()) diff --git a/src/lib/nfc/clf/__init__.py b/src/lib/nfc/clf/__init__.py new file mode 100644 index 0000000..c6154dc --- /dev/null +++ b/src/lib/nfc/clf/__init__.py @@ -0,0 +1,1251 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- + +import src.lib.nfc.tag +import src.lib.nfc.dep +import src.lib.nfc.llcp +from . import device + +import binascii +import os +import re +import time +import errno +import threading + +import logging +log = logging.getLogger(__name__) + + +def print_data(data): + return 'None' if data is None else binascii.hexlify(data).decode('latin') + + +class ContactlessFrontend(object): + """This class is the main interface for working with contactless + devices. The :meth:`connect` method provides easy access to the + contactless functionality through automated discovery of remote + cards and devices and activation of appropiate upper level + protocols for further interaction. The :meth:`sense`, + :meth:`listen` and :meth:`exchange` methods provide a low-level + interface for more specialized tasks. + + An instance of the :class:`ContactlessFrontend` class manages a + single contactless device locally connect through either USB, TTY + or COM port. A special UDP port driver allows for emulation of a + contactless device that connects through UDP to another emulated + contactless device for test and development of higher layer + functions. + + A locally connected contactless device can be opened by either + supplying a *path* argument when an an instance of the contactless + frontend class is created or by calling :meth:`open` at a later + time. In either case the *path* argument must be constructed as + described in :meth:`open` and the same exceptions may occur. The + difference is that :meth:`open` returns False if a device could + not be found whereas the initialization method raises + :exc:`~exceptions.IOError` with :data:`errno.ENODEV`. + + The methods of the :class:`ContactlessFrontend` class are + thread-safe. + + """ + def __init__(self, path=None): + self.device = None + self.target = None + self.lock = threading.Lock() + if path and not self.open(path): + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + def open(self, path): + """Open a contactless reader identified by the search *path*. + + The :meth:`open` method searches and then opens a contactless + reader device for further communication. The *path* argument + can be flexibly constructed to identify more or less precisely + the device to open. A *path* that only partially identifies a + device is completed by search. The first device that is found + and successfully opened causes :meth:`open` to return True. If + no device is found return value is False. If a device was + found but could not be opened then :meth:`open` returns False + if *path* was partial or raise :exc:`~exceptions.IOError` if + *path* was fully qualified. Typical I/O error reasons are + :data:`errno.EACCES` if the calling process has insufficient + access rights or :data:`errno.EBUSY` if the device is used by + another process. + + A path is constructed as follows: + + ``usb[:vendor[:product]]`` + + with optional *vendor* and *product* as four digit + hexadecimal numbers. For example, ``usb:054c:06c3`` would + open the first Sony RC-S380 reader while ``usb:054c`` would + open the first Sony reader found on USB. + + ``usb[:bus[:device]]`` + + with optional *bus* and *device* number as three-digit + decimals. For example, ``usb:001:023`` would open the + device enumerated as number 23 on bus 1 while ``usb:001`` + would open the first device found on bust 1. Note that a + new device number is generated every time the device is + plugged into USB. Bus and device numbers are shown by + ``lsusb``. + + ``tty:port:driver`` + + with mandatory *port* and *driver* name. This is for Posix + systems to open the serial port ``/dev/tty`` and use + the driver module ``nfc/dev/.py`` for access. For + example, ``tty:USB0:arygon`` would open ``/dev/ttyUSB0`` + and load the Arygon APPx/ADRx driver. + + ``com:port:driver`` + + with mandatory *port* and *driver* name. This is for + Windows systems to open the serial port ``COM`` and + use the driver module ``nfc/dev/.py`` for access. + + ``udp[:host][:port]`` + + with optional *host* name or address and *port* + number. This will emulate a communication channel over + UDP/IP. The defaults for *host* and *port* are + ``localhost:54321``. + + """ + if not isinstance(path, str): + raise TypeError("expecting a string type argument *path*") + if not len(path) > 0: + raise ValueError("argument *path* must not be empty") + + # Close current device driver if this is not the first + # open. This allows to use several devices sequentially or + # re-initialize a device. + self.close() + + # Acquire the lock and search for a device on *path* + with self.lock: + log.info("searching for reader on path " + path) + self.device = device.connect(path) + if self.device: + log.info("using {0}".format(self.device)) + else: + log.error("no reader available on path " + path) + return bool(self.device) + + def close(self): + """Close the contacless reader device.""" + with self.lock: + if self.device is not None: + try: + self.device.close() + except IOError: + pass + self.device = None + + def connect(self, **options): + """Connect with a Target or Initiator + + The calling thread is blocked until a single activation and + deactivation has completed or a callback function supplied as + the keyword argument ``terminate`` returns a true value. The + example below makes :meth:`~connect()` return after 5 seconds, + regardless of whether a peer device was connected or not. + + >>> import nfc, time + >>> clf = nfc.ContactlessFrontend('usb') + >>> after5s = lambda: time.time() - started > 5 + >>> started = time.time(); clf.connect(llcp={}, terminate=after5s) + + Connect options are given as keyword arguments with dictionary + values. Possible options are: + + * ``rdwr={key: value, ...}`` - options for reader/writer + * ``llcp={key: value, ...}`` - options for peer to peer + * ``card={key: value, ...}`` - options for card emulation + + **Reader/Writer Options** + + 'targets' : iterable + A list of bitrate and technology type strings that will + produce the :class:`~nfc.clf.RemoteTarget` objects to + discover. The default is ``('106A', '106B', '212F')``. + + 'on-startup' : function(targets) + This function is called before any attempt to discover a + remote card. The *targets* argument provides a list of + :class:`RemoteTarget` objects prepared from the 'targets' + bitrate and technology type strings. The function must + return a list of of those :class:`RemoteTarget` objects + that shall be finally used for discovery, those targets may + have additional attributes. An empty list or anything else + that evaluates false will remove the 'rdwr' option + completely. + + 'on-discover' : function(target) + This function is called when a :class:`RemoteTarget` has + been discovered. The *target* argument contains the + technology type specific discovery responses and should be + evaluated for multi-protocol support. The target will be + further activated only if this function returns a true + value. The default function depends on the 'llcp' option, + if present then the function returns True only if the + target does not indicate peer to peer protocol support, + otherwise it returns True for all targets. + + 'on-connect' : function(tag) + This function is called when a remote tag has been + activated. The *tag* argument is an instance of class + :class:`nfc.tag.Tag` and can be used for tag reading and + writing within the callback or in a separate thread. Any + true return value instructs :meth:`connect` to wait until + the tag is no longer present and then return True, any + false return value implies immediate return of the + :class:`nfc.tag.Tag` object. + + 'on-release' : function(tag) + This function is called when the presence check was run + (the 'on-connect' function returned a true value) and + determined that communication with the *tag* has become + impossible, or when the 'terminate' function returned a + true value. The *tag* object may be used for cleanup + actions but not for communication. + + 'iterations' : integer + This determines the number of sense cycles performed + between calls to the terminate function. Each iteration + searches once for all specified targets. The default value + is 5 iterations and between each iteration is a waiting + time determined by the 'interval' option described below. + As an effect of math there will be no waiting time if + iterations is set to 1. + + 'interval' : float + This determines the waiting time between iterations. The + default value of 0.5 seconds is considered a sensible + tradeoff between responsiveness in terms of tag discovery + and power consumption. It should be clear that changing + this value will impair one or the other. There is no free + beer. + + 'beep-on-connect': boolean + If the device supports beeping or flashing an LED, + automatically perform this functionality when a tag is + successfully detected AND the 'on-connect' function + returns a true value. Defaults to True. + + .. sourcecode:: python + + import nfc + + def on_startup(targets): + for target in targets: + target.sensf_req = bytearray.fromhex("0012FC0000") + return targets + + def on_connect(tag): + print(tag) + + rdwr_options = { + 'targets': ['212F', '424F'], + 'on-startup': on_startup, + 'on-connect': on_connect, + } + with nfc.ContactlessFrontend('usb') as clf: + tag = clf.connect(rdwr=rdwr_options) + if tag.ndef: + print(tag.ndef.message.pretty()) + + **Peer To Peer Options** + + 'on-startup' : function(llc) + This function is called before any attempt to establish + peer to peer communication. The *llc* argument provides the + :class:`~nfc.llcp.llc.LogicalLinkController` that may be + used to allocate and bind listen sockets for local + services. The function should return the *llc* object if + activation shall continue. Any other value removes the + 'llcp' option. + + 'on-connect' : function(llc) + This function is called when peer to peer communication is + successfully established. The *llc* argument provides the + now activated :class:`~nfc.llcp.llc.LogicalLinkController` + ready for allocation of client communication sockets and + data exchange in separate work threads. The function should + a true value return more or less immediately, unless it + wishes to handle the logical link controller run loop by + itself and anytime later return a false value. + + 'on-release' : function(llc) + This function is called when the symmetry loop was run (the + 'on-connect' function returned a true value) and determined + that communication with the remote peer has become + impossible, or when the 'terminate' function returned a + true value. The *llc* object may be used for cleanup + actions but not for communication. + + 'role' : string + This attribute determines whether the local device will + restrict itself to either ``'initiator'`` or ``'target'`` + mode of operation. As Initiator the local device will try + to discover a remote device. As Target it waits for being + discovered. The default is to alternate between both roles. + + 'miu' : integer + This attribute sets the maximum information unit size that + is announced to the remote device during link activation. + The default and also smallest possible value is 128 bytes. + + 'lto' : integer + This attribute sets the link timeout value (given in + milliseconds) that is announced to the remote device during + link activation. It informs the remote device that if the + local device does not return a protocol data unit before + the timeout expires, the communication link is broken and + can not be recovered. The *lto* is an important part of the + user experience, it ultimately tells when the user should + no longer expect communication to continue. The default + value is 500 millisecond. + + 'agf' : boolean + Some early phone implementations did not properly handle + aggregated protocol data units. This attribute allows to + disable the use af aggregation at the cost of efficiency. + Aggregation is disabled with a false value. The default + is to use aggregation. + + 'brs' : integer + For the local device in Initiator role the bit rate + selector determines the the bitrate to negotiate with the + remote Target. The value may be 0, 1, or 2 for 106, 212, or + 424 kbps, respectively. The default is to negotiate 424 + kbps. + + 'acm' : boolean + For the local device in Initiator role this attribute + determines whether a remote Target may also be activated in + active communication mode. In active communication mode + both peer devices mutually generate a radio field when + sending. The default is to use passive communication mode. + + 'rwt' : float + For the local device in Target role this attribute sets the + response waiting time announced during link activation. The + response waiting time is a medium access layer (NFC-DEP) + value that indicates when the remote Initiator shall + attempt error recovery after missing a Target response. The + value is the waiting time index *wt* that determines the + effective response waiting time by the formula ``rwt = + 4096/13.56E6 * pow(2, wt)``. The value shall not be greater + than 14. The default value is 8 and yields an effective + response waiting time of 77.33 ms. + + 'lri' : integer + For the local device in Initiator role this attribute sets + the length reduction for medium access layer (NFC-DEP) + information frames. The value may be 0, 1, 2, or 3 for a + maximum payload size of 64, 128, 192, or 254 bytes, + respectively. The default value is 3. + + 'lrt' : integer + For the local device in Target role this attribute sets + the length reduction for medium access layer (NFC-DEP) + information frames. The value may be 0, 1, 2, or 3 for a + maximum payload size of 64, 128, 192, or 254 bytes, + respectively. The default value is 3. + + .. sourcecode:: python + + import nfc + import nfc.llcp + import threading + + def server(socket): + message, address = socket.recvfrom() + socket.sendto("It's me!", address) + socket.close() + + def client(socket): + socket.sendto("Hi there!", address=32) + socket.close() + + def on_startup(llc): + socket = nfc.llcp.Socket(llc, nfc.llcp.LOGICAL_DATA_LINK) + socket.bind(address=32) + threading.Thread(target=server, args=(socket,)).start() + return llc + + def on_connect(llc): + socket = nfc.llcp.Socket(llc, nfc.llcp.LOGICAL_DATA_LINK) + threading.Thread(target=client, args=(socket,)).start() + return True + + llcp_options = { + 'on-startup': on_startup, + 'on-connect': on_connect, + } + with nfc.ContactlessFrontend('usb') as clf: + clf.connect(llcp=llcp_options) + print("link terminated") + + **Card Emulation Options** + + 'on-startup' : function(target) + This function is called to prepare a local target for + discovery. The input argument is a fresh instance of an + unspecific :class:`LocalTarget` that can be set to the + desired bitrate and modulation type and populated with the + type specific discovery responses (see :meth:`listen` for + response data that is needed). The fully specified target + object must then be returned. + + 'on-discover' : function(target) + This function is called when the :class:`LocalTarget` has + been discovered. The *target* argument contains the + technology type specific discovery commands. The target + will be further activated only if this function returns a + true value. The default function always returns True. + + 'on-connect' : function(tag) + This function is called when the local target was + discovered and a :class:`nfc.tag.TagEmulation` object + successfully initialized. The function receives the + emulated *tag* object which stores the first command + received after inialization as ``tag.cmd``. The function + should return a true value if the tag.process_command() and + tag.send_response() methods shall be called repeatedly + until either the remote device terminates communication or + the 'terminate' function returns a true value. The function + should return a false value if the :meth:`connect` method + shall return immediately with the emulated *tag* object. + + 'on-release' : function(tag) + This function is called when the Target was released by the + Initiator or simply moved away, or if the terminate + callback function has returned a true value. The emulated + *tag* object may be used for cleanup actions but not for + communication. + + .. sourcecode:: python + + import nfc + + def on_startup(target): + idm = bytearray.fromhex("01010501b00ac30b") + pmm = bytearray.fromhex("03014b024f4993ff") + sys = bytearray.fromhex("1234") + target.brty = "212F" + target.sensf_res = chr(1) + idm + pmm + sys + return target + + def on_connect(tag): + print("discovered by remote reader") + return True + + def on_release(tag): + print("remote reader is gone") + return True + + card_options = { + 'on-startup': on_startup, + 'on-connect': on_connect, + 'on-release': on_release, + } + with nfc.ContactlessFrontend('usb') as clf: + clf.connect(card=card_options) + + **Return Value** + + The :meth:`connect` method returns :const:`None` if there were + no options left after the 'on-startup' functions have been + executed or when the 'terminate' function returned a true + value. It returns :const:`False` when terminated by any of the + following exceptions: :exc:`~exceptions.KeyboardInterrupt`, + :exc:`~exceptions.IOError`, :exc:`UnsupportedTargetError`. + + The :meth:`connect` method returns a :class:`~nfc.tag.Tag`, + :class:`~nfc.llcp.llc.LogicalLinkController`, or + :class:`~nfc.tag.TagEmulation` object if the associated + 'on-connect' function returned a false value to indicate that + it will handle presence check, peer to peer symmetry loop, or + command/response processing by itself. + + """ + if self.device is None: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + log.debug("connect{0}".format( + tuple([k for k in options if options[k]]))) + + terminate = options.get('terminate', lambda: False) + rdwr_options = options.get('rdwr') + llcp_options = options.get('llcp') + card_options = options.get('card') + + try: + assert isinstance(rdwr_options, (dict, type(None))), "rdwr" + assert isinstance(llcp_options, (dict, type(None))), "llcp" + assert isinstance(card_options, (dict, type(None))), "card" + except AssertionError as error: + raise TypeError("argument '%s' must be a dictionary" % error) + + if llcp_options is not None: + llcp_options = dict(llcp_options) + llcp_options.setdefault('on-startup', lambda llc: llc) + llcp_options.setdefault('on-connect', lambda llc: True) + llcp_options.setdefault('on-release', lambda llc: True) + + llc = nfc.llcp.llc.LogicalLinkController(**llcp_options) + llc = llcp_options['on-startup'](llc) + if isinstance(llc, nfc.llcp.llc.LogicalLinkController): + llcp_options['llc'] = llc + else: + log.debug("removing llcp_options after on-startup") + llcp_options = None + + if rdwr_options is not None: + def on_discover(target): + if target.sel_res and target.sel_res[0] & 0x40: + return False + elif target.sensf_res and target.sensf_res[1:3] == b"\x01\xFE": + return False + else: + return True + + rdwr_options = dict(rdwr_options) + rdwr_options.setdefault('targets', ['106A', '106B', '212F']) + rdwr_options.setdefault('on-startup', lambda targets: targets) + rdwr_options.setdefault('on-discover', on_discover) + rdwr_options.setdefault('on-connect', lambda tag: True) + rdwr_options.setdefault('on-release', lambda tag: True) + rdwr_options.setdefault('iterations', 5) + rdwr_options.setdefault('interval', 0.5) + rdwr_options.setdefault('beep-on-connect', True) + + targets = [RemoteTarget(brty) for brty in rdwr_options['targets']] + targets = rdwr_options['on-startup'](targets) + if targets and all([isinstance(o, RemoteTarget) for o in targets]): + rdwr_options['targets'] = targets + else: + log.debug("removing rdwr_options after on-startup") + rdwr_options = None + + if card_options is not None: + card_options = dict(card_options) + card_options.setdefault('on-startup', lambda target: None) + card_options.setdefault('on-discover', lambda target: True) + card_options.setdefault('on-connect', lambda tag: True) + card_options.setdefault('on-release', lambda tag: True) + + target = nfc.clf.LocalTarget() + target = card_options['on-startup'](target) + if isinstance(target, LocalTarget): + card_options['target'] = target + else: + log.debug("removing card_options after on-startup") + card_options = None + + if not (rdwr_options or llcp_options or card_options): + log.warning("no options to connect") + return None + + log.debug("connect options after startup: %s", + ', '.join(filter(bool, ["rdwr" if rdwr_options else None, + "llcp" if llcp_options else None, + "card" if card_options else None]))) + + try: + while not terminate(): + if rdwr_options: + result = self._rdwr_connect(rdwr_options, terminate) + if bool(result) is True: + return result + if llcp_options: + result = self._llcp_connect(llcp_options, terminate) + if bool(result) is True: + return result + if card_options: + result = self._card_connect(card_options, terminate) + if bool(result) is True: + return result + except IOError as error: + log.error(error) + return False + except UnsupportedTargetError as error: + log.info(error) + return False + except KeyboardInterrupt: + log.debug("terminated by keyboard interrupt") + return False + + def _rdwr_connect(self, options, terminate): + target = self.sense(*options['targets'], + iterations=options['iterations'], + interval=options['interval']) + if target is not None: + log.debug("discovered target {0}".format(target)) + if options['on-discover'](target): + tag = nfc.tag.activate(self, target) + if tag is not None: + log.debug("connected to {0}".format(tag)) + if options['on-connect'](tag): + if options['beep-on-connect']: + self.device.turn_on_led_and_buzzer() + while not terminate() and tag.is_present: + time.sleep(0.1) + self.device.turn_off_led_and_buzzer() + return options['on-release'](tag) + else: + return tag + + def _llcp_connect(self, options, terminate): + llc = options['llc'] + for role in ('target', 'initiator'): + if options.get('role') is None or options.get('role') == role: + DEP = eval("nfc.dep." + role.capitalize()) + dep_cfg = ('brs', 'acm', 'rwt', 'lrt', 'lri') + dep_cfg = {k: options[k] for k in dep_cfg if k in options} + if llc.activate(mac=DEP(clf=self), **dep_cfg): + log.debug("connected {0}".format(llc)) + if options['on-connect'](llc): + llc.run(terminate=terminate) + return options['on-release'](llc) + else: + return llc + + def _card_connect(self, options, terminate): + timeout = options.get('timeout', 1.0) + target = self.listen(options['target'], timeout) + if target and options['on-discover'](target): + log.debug("activated as {0}".format(target)) + tag = nfc.tag.emulate(self, target) + if isinstance(tag, nfc.tag.TagEmulation): + log.debug("connected as {0}".format(tag)) + if options['on-connect'](tag): + tag_rsp = tag.process_command(tag.cmd) + while not terminate(): + try: + tag_cmd = tag.send_response(tag_rsp, None) + tag_rsp = tag.process_command(tag_cmd) + except nfc.clf.BrokenLinkError as error: + log.debug(error) + break + except nfc.clf.CommunicationError as error: + log.debug(error) + tag_rsp = None + return options['on-release'](tag) + else: + return tag + + def sense(self, *targets, **options): + """Discover a contactless card or listening device. + + .. note:: The :meth:`sense` method is intended for experts + with a good understanding of the commands and + responses exchanged during target activation (the + notion used for commands and responses follows the + NFC Forum Digital Specification). If the greater + level of control is not needed it is recommended to + use the :meth:`connect` method. + + All positional arguments build the list of potential *targets* + to discover and must be of type :class:`RemoteTarget`. Keyword + argument *options* may be the number of ``iterations`` of the + sense loop set by *targets* and the ``interval`` between + iterations. The return value is either a :class:`RemoteTarget` + instance or :const:`None`. + + >>> import nfc, nfc.clf + >>> from binascii import hexlify + >>> clf = nfc.ContactlessFrontend("usb") + >>> target1 = nfc.clf.RemoteTarget("106A") + >>> target2 = nfc.clf.RemoteTarget("212F") + >>> print(clf.sense(target1, target2, iterations=5, interval=0.2)) + 106A(sdd_res=04497622D93881, sel_res=00, sens_res=4400) + + A **Type A Target** is specified with the technology letter + ``A`` following the bitrate to be used for the SENS_REQ + command (almost always must the bitrate be 106 kbps). To + discover only a specific Type A target, the NFCID1 (UID) can + be set with a 4, 7, or 10 byte ``sel_req`` attribute (cascade + tags are handled internally). + + >>> target = nfc.clf.RemoteTarget("106A") + >>> print(clf.sense(target)) + 106A sdd_res=04497622D93881 sel_res=00 sens_res=4400 + >>> target.sel_req = bytearray.fromhex("04497622D93881") + >>> print(clf.sense(target)) + 106A sdd_res=04497622D93881 sel_res=00 sens_res=4400 + >>> target.sel_req = bytearray.fromhex("04497622") + >>> print(clf.sense(target)) + None + + A **Type B Target** is specified with the technology letter + ``B`` following the bitrate to be used for the SENSB_REQ + command (almost always must the bitrate be 106 kbps). A + specific application family identifier can be set with the + first byte of a ``sensb_req`` attribute (the second byte PARAM + is ignored when it can not be set to local device, 00h is a + safe value in all cases). + + >>> target = nfc.clf.RemoteTarget("106B") + >>> print(clf.sense(target)) + 106B sens_res=50E5DD3DC900000011008185 + >>> target.sensb_req = bytearray.fromhex("0000") + >>> print(clf.sense(target)) + 106B sens_res=50E5DD3DC900000011008185 + >>> target.sensb_req = bytearray.fromhex("FF00") + >>> print(clf.sense(target)) + None + + A **Type F Target** is specified with the technology letter + ``F`` following the bitrate to be used for the SENSF_REQ + command (the typically supported bitrates are 212 and 424 + kbps). The default SENSF_REQ command allows all targets to + answer, requests system code information, and selects a single + time slot for the SENSF_RES response. This can be changed with + the ``sensf_req`` attribute. + + >>> target = nfc.clf.RemoteTarget("212F") + >>> print(clf.sense(target)) + 212F sensf_res=0101010601B00ADE0B03014B024F4993FF12FC + >>> target.sensf_req = bytearray.fromhex("0012FC0000") + >>> print(clf.sense(target)) + 212F sensf_res=0101010601B00ADE0B03014B024F4993FF + >>> target.sensf_req = bytearray.fromhex("00ABCD0000") + >>> print(clf.sense(target)) + None + + An **Active Communication Mode P2P Target** search is selected + with an ``atr_req`` attribute. The choice of bitrate and + modulation type is 106A, 212F, and 424F. + + >>> atr = bytearray.fromhex("D4000102030405060708091000000030") + >>> target = clf.sense(nfc.clf.RemoteTarget("106A", atr_req=atr)) + >>> if target and target.atr_res: + >>> print(hexlify(target.atr_res).decode()) + d501c023cae6b3182afe3dee0000000e3246666d01011103020013040196 + >>> target = clf.sense(nfc.clf.RemoteTarget("424F", atr_req=atr)) + >>> if target and target.atr_res: + >>> print(hexlify(target.atr_res).decode()) + d501dc0104f04584e15769700000000e3246666d01011103020013040196 + + Some drivers must modify the ATR_REQ to cope with hardware + limitations, for example change length reduction value to + reduce the maximum size of target responses. The ATR_REQ that + has been send is given by the ``atr_req`` attribute of the + returned RemoteTarget object. + + A **Passive Communication Mode P2P Target** responds to 106A + discovery with bit 6 of SEL_RES set to 1, and to 212F/424F + discovery (when the request code RC is 0 in the SENSF_REQ + command) with an NFCID2 that starts with 01FEh in the + SENSF_RES response. Responses below are from a Nexus 5 + configured for NFC-DEP Protocol (SEL_RES bit 6 is set) and + Type 4A Tag (SEL_RES bit 5 is set). + + >>> print(clf.sense(nfc.clf.RemoteTarget("106A"))) + 106A sdd_res=08796BEB sel_res=60 sens_res=0400 + >>> sensf_req = bytearray.fromhex("00FFFF0000") + >>> print(clf.sense(nfc.clf.RemoteTarget("424F", sensf_req=sensf_req))) + 424F sensf_res=0101FE1444EFB88FD50000000000000000 + + Errors found in the *targets* argument list raise exceptions + only if exactly one target is given. If multiple targets are + provided, any target that is not supported or has invalid + attributes is just ignored (but is logged as a debug message). + + **Exceptions** + + * :exc:`~exceptions.IOError` (ENODEV) when a local contacless + communication device has not been opened or communication + with the local device is no longer possible. + + * :exc:`nfc.clf.UnsupportedTargetError` if the single target + supplied as input is not supported by the active driver. + This exception is never raised when :meth:`sense` is called + with multiple targets, those unsupported are then silently + ignored. + + """ + def sense_tta(target): + if target.sel_req and len(target.sel_req) not in (4, 7, 10): + raise ValueError("sel_req must be 4, 7, or 10 byte") + target = self.device.sense_tta(target) + log.debug("found %s", target) + if target and len(target.sens_res) != 2: + error = "SENS Response Format Error (wrong length)" + log.debug(error) + raise ProtocolError(error) + if target and target.sens_res[0] & 0b00011111 == 0: + if target.sens_res[1] & 0b00001111 != 0b1100: + error = "SENS Response Data Error (T1T config)" + log.debug(error) + raise ProtocolError(error) + if not target.rid_res: + error = "RID Response Error (no response received)" + log.debug(error) + raise ProtocolError(error) + if len(target.rid_res) != 6: + error = "RID Response Format Error (wrong length)" + log.debug(error) + raise ProtocolError(error) + if target.rid_res[0] >> 4 != 0b0001: + error = "RID Response Data Error (invalid HR0)" + log.debug(error) + raise ProtocolError(error) + return target + + def sense_ttb(target): + return self.device.sense_ttb(target) + + def sense_ttf(target): + return self.device.sense_ttf(target) + + def sense_dep(target): + if len(target.atr_req) < 16: + raise ValueError("minimum atr_req length is 16 byte") + if len(target.atr_req) > 64: + raise ValueError("maximum atr_req length is 64 byte") + return self.device.sense_dep(target) + + for target in targets: + if not isinstance(target, RemoteTarget): + raise ValueError("invalid target argument type: %r" % target) + + with self.lock: + if self.device is None: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + self.target = None # forget captured target + self.device.mute() # deactivate the rf field + + for i in range(max(1, options.get('iterations', 1))): + started = time.time() + for target in targets: + log.debug("sense {0}".format(target)) + try: + if target.atr_req is not None: + self.target = sense_dep(target) + elif target.brty.endswith('A'): + self.target = sense_tta(target) + elif target.brty.endswith('B'): + self.target = sense_ttb(target) + elif target.brty.endswith('F'): + self.target = sense_ttf(target) + else: + info = "unknown technology type in %r" + raise UnsupportedTargetError(info % target.brty) + except UnsupportedTargetError as error: + if len(targets) == 1: + raise error + else: + log.debug(error) + except CommunicationError as error: + log.debug(error) + else: + if self.target is not None: + log.debug("found {0}".format(self.target)) + return self.target + if len(targets) > 0: + self.device.mute() # deactivate the rf field + if i < options.get('iterations', 1) - 1: + elapsed = time.time() - started + time.sleep(max(0, options.get('interval', 0.1)-elapsed)) + + def listen(self, target, timeout): + """Listen *timeout* seconds to become activated as *target*. + + .. note:: The :meth:`listen` method is intended for experts + with a good understanding of the commands and + responses exchanged during target activation (the + notion used for commands and responses follows the + NFC Forum Digital Specification). If the greater + level of control is not needed it is recommended to + use the :meth:`connect` method. + + The *target* argument is a :class:`LocalTarget` object that + provides bitrate, technology type and response data + attributes. The return value is either a :class:`LocalTarget` + object with bitrate, technology type and request/response data + attributes or :const:`None`. + + An **P2P Target** is selected when the ``atr_res`` attribute + is set. The bitrate and technology type are decided by the + Initiator and do not need to be specified. The ``sens_res``, + ``sdd_res`` and ``sel_res`` attributes for Type A technology + as well as the ``sensf_res`` attribute for Type F technolgy + must all be set. + + When activated, the bitrate and type are set to the current + communication values, the ``atr_req`` attribute contains the + ATR_REQ received from the Initiator and the ``dep_req`` + attribute contains the first DEP_REQ received after + activation. If the Initiator has changed communication + parameters, the ``psl_req`` attribute holds the PSL_REQ that + was received. The ``atr_res`` (and the ``psl_res`` if + transmitted) are also made available. + + If the local target was activated in passive communication + mode either the Type A response (``sens_res``, ``sdd_res``, + ``sel_res``) or Type F response (``sensf_res``) attributes + will be present. + + With a Nexus 5 on a reader connected via USB the following + code should be working and produce similar output (the Nexus 5 + prioritizes active communication mode): + + >>> import nfc, nfc.clf + >>> clf = nfc.ContactlessFrontend("usb") + >>> atr_res = "d50101fe0102030405060708000000083246666d010110" + >>> target = nfc.clf.LocalTarget() + >>> target.sensf_res = bytearray.fromhex("0101FE"+16*"FF") + >>> target.sens_res = bytearray.fromhex("0101") + >>> target.sdd_res = bytearray.fromhex("08010203") + >>> target.sel_res = bytearray.fromhex("40") + >>> target.atr_res = bytearray.fromhex(atr_res) + >>> print(clf.listen(target, timeout=2.5)) + 424F atr_res=D50101FE0102030405060708000000083246666D010110 ... + + A **Type A Target** is selected when ``atr_res`` is not + present and the technology type is ``A``. The bitrate should + be set to 106 kbps, even if a driver supports higher bitrates + they would need to be set after activation. The ``sens_res``, + ``sdd_res`` and ``sel_res`` attributes must all be provided. + + >>> target = nfc.clf.Localtarget("106A") + >>> target.sens_res = bytearray.fromhex("0101")) + >>> target.sdd_res = bytearray.fromhex("08010203") + >>> target.sel_res = bytearray.fromhex("00") + >>> print(clf.listen(target, timeout=2.5)) + 106A sdd_res=08010203 sel_res=00 sens_res=0101 tt2_cmd=3000 + + A **Type B Target** is selected when ``atr_res`` is not + present and the technology type is ``B``. Unfortunately none + of the supported devices supports Type B technology for listen + and an :exc:`nfc.clf.UnsupportedTargetError` exception will be + the only result. + + >>> target = nfc.clf.LocalTarget("106B") + >>> try: clf.listen(target, 2.5) + ... except nfc.clf.UnsupportedTargetError: print("sorry") + ... + sorry + + A **Type F Target** is selected when ``atr_res`` is not + present and the technology type is ``F``. The bitrate may be + 212 or 424 kbps. The ``sensf_res`` attribute must be provided. + + >>> idm, pmm, sys = "02FE010203040506", "FFFFFFFFFFFFFFFF", "12FC" + >>> target = nfc.clf.LocalTarget("212F") + >>> target.sensf_res = bytearray.fromhex("01" + idm + pmm + sys) + >>> print(clf.listen(target, 2.5)) + 212F sensf_req=00FFFF0003 tt3_cmd=0C02FE010203040506 ... + + **Exceptions** + + * :exc:`~exceptions.IOError` (ENODEV) when a local contacless + communication device has not been opened or communication + with the local device is no longer possible. + + * :exc:`nfc.clf.UnsupportedTargetError` if the single target + supplied as input is not supported by the active driver. + This exception is never raised when :meth:`sense` is called + with multiple targets, those unsupported are then silently + ignored. + + """ + def listen_tta(target, timeout): + return self.device.listen_tta(target, timeout) + + def listen_ttb(target, timeout): + return self.device.listen_ttb(target, timeout) + + def listen_ttf(target, timeout): + return self.device.listen_ttf(target, timeout) + + def listen_dep(target, timeout): + target = self.device.listen_dep(target, timeout) + if target and target.atr_req: + try: + assert len(target.atr_req) >= 16, "less than 16 byte" + assert len(target.atr_req) <= 64, "more than 64 byte" + return target + except AssertionError as error: + log.debug("atr_req is %s", str(error)) + + assert isinstance(target, LocalTarget), \ + "invalid target argument type: %r" % target + + with self.lock: + if self.device is None: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + self.target = None # forget captured target + self.device.mute() # deactivate the rf field + + info = "listen %.3f seconds for %s" + if target.atr_res is not None: + log.debug(info, timeout, "DEP") + self.target = listen_dep(target, timeout) + elif target.brty in ('106A', '212A', '424A'): + log.debug(info, timeout, target) + self.target = listen_tta(target, timeout) + elif target.brty in ('106B', '212B', '424B', '848B'): + log.debug(info, timeout, target) + self.target = listen_ttb(target, timeout) + elif target.brty in ('212F', '424F'): + log.debug(info, timeout, target) + self.target = listen_ttf(target, timeout) + else: + errmsg = "unsupported bitrate technology type {}" + raise ValueError(errmsg.format(target.brty)) + + return self.target + + def exchange(self, send_data, timeout): + """Exchange data with an activated target (*send_data* is a command + frame) or as an activated target (*send_data* is a response + frame). Returns a target response frame (if data is send to an + activated target) or a next command frame (if data is send + from an activated target). Returns None if the communication + link broke during exchange (if data is sent as a target). The + timeout is the number of seconds to wait for data to return, + if the timeout expires an nfc.clf.TimeoutException is + raised. Other nfc.clf.CommunicationError exceptions may be raised if + an error is detected during communication. + + """ + with self.lock: + if self.device is None: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + log.debug(">>> %s timeout=%s", print_data(send_data), str(timeout)) + + if isinstance(self.target, RemoteTarget): + exchange = self.device.send_cmd_recv_rsp + elif isinstance(self.target, LocalTarget): + exchange = self.device.send_rsp_recv_cmd + else: + log.error("no target for data exchange") + return None + + send_time = time.time() + rcvd_data = exchange(self.target, send_data, timeout) + recv_time = time.time() - send_time + + log.debug("<<< %s %.3fs", print_data(rcvd_data), recv_time) + return rcvd_data + + @property + def max_send_data_size(self): + """The maximum number of octets that can be send with the + :meth:`exchange` method in the established operating mode. + + """ + with self.lock: + if self.device is None: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + else: + return self.device.get_max_send_data_size(self.target) + + @property + def max_recv_data_size(self): + """The maximum number of octets that can be received with the + :meth:`exchange` method in the established operating mode. + + """ + with self.lock: + if self.device is None: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + else: + return self.device.get_max_recv_data_size(self.target) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def __str__(self): + if self.device is not None: + s = "{dev.vendor_name} {dev.product_name} on {dev.path}" + return s.format(dev=self.device) + else: + return self.__repr__() + + +############################################################################### +# +# Targets +# +############################################################################### +class Target(object): + def __init__(self, **kwargs): + for name in kwargs: + self.__dict__[name] = kwargs[name] + + def __getattr__(self, name): + return None + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + def __str__(self): + attrs = [] + for name in sorted(self.__dict__.keys()): + if name.startswith('_'): + continue + value = self.__dict__[name] + if isinstance(value, (bytes, bytearray)): + value = binascii.hexlify(value).decode().upper() + attrs.append("{0}={1}".format(name, value)) + return "{brty} {attrs}".format(brty=self.brty, attrs=' '.join(attrs)) + + +class RemoteTarget(Target): + """A RemoteTarget instance provides bitrate and technology type and + command/response data of a remote card or device that, when input + to :meth:`sense`, shall be attempted to discover and, when + returned by :meth:`sense`, has been discovered by the local + device. Command/response data attributes, whatever name, default + to None. + + """ + brty_pattern = re.compile(r'(\d+[A-Z])(?:/(\d+[A-Z])|.*)') + + def __init__(self, brty, **kwargs): + super(RemoteTarget, self).__init__(**kwargs) + self.brty = brty + + @property + def brty(self): + """A string that combines bitrate and technology type, e.g. '106A'.""" + return self._brty_send + + @brty.setter + def brty(self, value): + brty_pattern_match = self.brty_pattern.match(value) + if brty_pattern_match: + (self._brty_send, self._brty_recv) = brty_pattern_match.groups() + if not self._brty_recv: + self._brty_recv = self._brty_send + else: + raise ValueError("brty pattern does not match for %r" % value) + + @property + def brty_send(self): + return self._brty_send + + @property + def brty_recv(self): + return self._brty_recv + + +class LocalTarget(Target): + """A LocalTarget instance provides bitrate and technology type and + command/response data of the local card or device that, when input + to :meth:`listen`, shall be made available for discovery and, when + returned by :meth:`listen`, has been discovered by a remote + device. Command/response data attributes, whatever name, default + to None. + + """ + def __init__(self, brty='106A', **kwargs): + super(LocalTarget, self).__init__(**kwargs) + self.brty = brty + + @property + def brty(self): + """A string that combines bitrate and technology type, e.g. '106A'.""" + return self._brty_send \ + if self._brty_send == self._brty_recv \ + else self._brty_send+"/"+self._brty_recv + + @brty.setter + def brty(self, value): + self._brty_send = self._brty_recv = value + + +############################################################################### +# +# Exceptions +# +############################################################################### +class Error(Exception): + """Base class for exceptions specific to the contacless frontend module. + + - UnsupportedTargetError + - CommunicationError + + - ProtocolError + - TransmissionError + - TimeoutError + - BrokenLinkError + + """ + + +class UnsupportedTargetError(Error): + """The :class:`RemoteTarget` input to + :meth:`ContactlessFrontend.sense` or :class:`LocalTarget` input to + :meth:`ContactlessFrontend.listen` is not supported by the local + device. + + """ + + +class CommunicationError(Error): + """Base class for communication errors. + + """ + + +class ProtocolError(CommunicationError): + """Raised when an NFC Forum Digital Specification protocol error + occured. + + """ + + +class TransmissionError(CommunicationError): + """Raised when an NFC Forum Digital Specification transmission error + occured. + + """ + + +class TimeoutError(CommunicationError): + """Raised when an NFC Forum Digital Specification timeout error + occured. + + """ + + +class BrokenLinkError(CommunicationError): + """The remote device (Reader/Writer or P2P Device) has deactivated the + RF field or is no longer within communication distance. + + """ diff --git a/src/lib/nfc/clf/acr122.py b/src/lib/nfc/clf/acr122.py new file mode 100644 index 0000000..54b647b --- /dev/null +++ b/src/lib/nfc/clf/acr122.py @@ -0,0 +1,242 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2011, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""Device driver for the Arygon ACR122U contactless reader. + +The Arygon ACR122U is a PC/SC compliant contactless reader that +connects via USB and uses the USB CCID profile. It is normally +intented to be used with a PC/SC stack but this driver interfaces +directly with the inbuilt PN532 chipset by tunneling commands through +the PC/SC Escape command. The driver is limited in functionality +because the embedded microprocessor (that implements the PC/SC stack) +also operates the PN532; it does not allow all commands to pass as +desired and reacts on chip responses with its own (legitimate) +interpretation of state. + +========== ======= ============ +function support remarks +========== ======= ============ +sense_tta yes Type 1 (Topaz) Tags are not supported +sense_ttb yes ATTRIB by firmware voided with S(DESELECT) +sense_ttf yes +sense_dep yes +listen_tta no +listen_ttb no +listen_ttf no +listen_dep no +========== ======= ============ + +""" +import nfc.clf +from . import pn532 + +import os +import errno +import struct +from binascii import hexlify + +import logging +log = logging.getLogger(__name__) + + +def init(transport): + device = Device(Chipset(transport)) + device._vendor_name = transport.manufacturer_name + device._device_name = transport.product_name.split()[0] + return device + + +class Device(pn532.Device): + # Device driver class for the ACR122U. + + def __init__(self, chipset): + super(Device, self).__init__(chipset, logger=log) + + def sense_tta(self, target): + """Activate the RF field and probe for a Type A Target at 106 + kbps. Other bitrates are not supported. Type 1 Tags are not + supported because the device does not allow to send the + correct RID command (even though the PN532 does). + + """ + return super(Device, self).sense_tta(target) + + def sense_ttb(self, target): + """Activate the RF field and probe for a Type B Target. + + The RC-S956 can discover Type B Targets (Type 4B Tag) at 106 + kbps. For a Type 4B Tag the firmware automatically sends an + ATTRIB command that configures the use of DID and 64 byte + maximum frame size. The driver reverts this configuration with + a DESELECT and WUPB command to return the target prepared for + activation (which nfcpy does in the tag activation code). + + """ + return super(Device, self).sense_ttb(target) + + def sense_ttf(self, target): + """Activate the RF field and probe for a Type F Target. Bitrates 212 + and 424 kpbs are supported. + + """ + return super(Device, self).sense_ttf(target) + + def sense_dep(self, target): + """Search for a DEP Target. Both passive and passive communication + mode are supported. + + """ + return super(Device, self).sense_dep(target) + + def listen_tta(self, target, timeout): + """Listen as Type A Target is not supported.""" + info = "{device} does not support listen as Type A Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_ttb(self, target, timeout): + """Listen as Type B Target is not supported.""" + info = "{device} does not support listen as Type B Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_ttf(self, target, timeout): + """Listen as Type F Target is not supported.""" + info = "{device} does not support listen as Type F Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_dep(self, target, timeout): + """Listen as DEP Target is not supported.""" + info = "{device} does not support listen as DEP Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def turn_on_led_and_buzzer(self): + """Buzz and turn red.""" + self.chipset.set_buzzer_and_led_to_active() + + def turn_off_led_and_buzzer(self): + """Back to green.""" + self.chipset.set_buzzer_and_led_to_default() + + +class Chipset(pn532.Chipset): + # Maximum size of a host command frame to the contactless chip. + host_command_frame_max_size = 254 + + # Supported BrTy (baud rate / modulation type) values for the + # InListPassiveTarget command. Corresponds to 106 kbps Type A, 212 + # kbps Type F, 424 kbps Type F, and 106 kbps Type B. The value for + # 106 kbps Innovision Jewel Tag (although supported by PN532) is + # removed because the RID command can not be send. + in_list_passive_target_brty_range = (0, 1, 2, 3) + + def __init__(self, transport): + self.transport = transport + + # read ACR122U firmware version string + reader_version = self.ccid_xfr_block(bytearray.fromhex("FF00480000")) + if not reader_version.startswith(b"ACR122U"): + log.error("failed to retrieve ACR122U version string") + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + if int(chr(reader_version[7])) < 2: + log.error("{0} not supported, need 2.x".format(reader_version[7:])) + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + log.debug("initialize " + reader_version.decode()) + + # set icc power on + log.debug("CCID ICC-POWER-ON") + frame = bytearray.fromhex("62000000000000000000") + transport.write(frame) + transport.read(100) + + # disable autodetection + log.debug("Set PICC Operating Parameters") + self.ccid_xfr_block(bytearray.fromhex("FF00517F00")) + + # switch red/green led off/on + log.debug("Configure Buzzer and LED") + self.set_buzzer_and_led_to_default() + + super(Chipset, self).__init__(transport, logger=log) + + def close(self): + self.ccid_xfr_block(bytearray.fromhex("FF00400C0400000000")) + self.transport.close() + self.transport = None + + def set_buzzer_and_led_to_default(self): + """Turn off buzzer and set LED to default (green only). """ + self.ccid_xfr_block(bytearray.fromhex("FF00400E0400000000")) + + def set_buzzer_and_led_to_active(self, duration_in_ms=300): + """Turn on buzzer and set LED to red only. The timeout here must exceed + the total buzzer/flash duration defined in bytes 5-8. """ + duration_in_tenths_of_second = int(min(duration_in_ms / 100, 255)) + timeout_in_seconds = (duration_in_tenths_of_second + 1) / 10.0 + data = "FF00400D04{:02X}000101".format(duration_in_tenths_of_second) + self.ccid_xfr_block(bytearray.fromhex(data), + timeout=timeout_in_seconds) + + def send_ack(self): + # Send an ACK frame, usually to terminate most recent command. + self.ccid_xfr_block(Chipset.ACK) + + def ccid_xfr_block(self, data, timeout=0.1): + """Encapsulate host command *data* into an PC/SC Escape command to + send to the device and extract the chip response if received + within *timeout* seconds. + + """ + frame = struct.pack(" +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +# +# Driver for the Arygon contactless reader with USB serial interface +# +from . import pn531 +from . import pn532 + +import os +import time +import errno + +import logging +log = logging.getLogger(__name__) + + +class ChipsetA(pn531.Chipset): + def write_frame(self, frame): + self.transport.write(b"2" + frame) + + +class DeviceA(pn531.Device): + def close(self): + self.chipset.transport.tty.write(b"0au") # device reset + self.chipset.close() + self.chipset = None + + +class ChipsetB(pn532.Chipset): + def write_frame(self, frame): + self.transport.write(b"2" + frame) + + +class DeviceB(pn532.Device): + def close(self): + self.chipset.transport.tty.write(b"0au") # device reset + self.chipset.close() + self.chipset = None + + +def init(transport): + transport.open(transport.port, 115200) + transport.tty.write(b"0av") # read version + response = transport.tty.readline() + if response.startswith(b"FF00000600V"): + log.debug("Arygon Reader AxxB Version %s", + response[11:].strip().decode()) + transport.tty.timeout = 0.5 + transport.tty.write(b"0at05") + if transport.tty.readline().startswith(b"FF0000"): + log.debug("MCU/TAMA communication set to 230400 bps") + transport.tty.write(b"0ah05") + if transport.tty.readline().startswith(b"FF0000"): + log.debug("MCU/HOST communication set to 230400 bps") + transport.tty.baudrate = 230400 + transport.tty.timeout = 0.1 + time.sleep(0.1) + chipset = ChipsetB(transport, logger=log) + device = DeviceB(chipset, logger=log) + device._vendor_name = "Arygon" + device._device_name = "ADRB" + return device + + transport.open(transport.port, 9600) + transport.tty.write(b"0av") # read version + response = transport.tty.readline() + if response.startswith(b"FF00000600V"): + log.debug("Arygon Reader AxxA Version %s", + response[11:].strip().decode()) + transport.tty.timeout = 0.5 + transport.tty.write(b"0at05") + if transport.tty.readline().startswith(b"FF0000"): + log.debug("MCU/TAMA communication set to 230400 bps") + transport.tty.write(b"0ah05") + if transport.tty.readline().startswith(b"FF0000"): + log.debug("MCU/HOST communication set to 230400 bps") + transport.tty.baudrate = 230400 + transport.tty.timeout = 0.1 + time.sleep(0.1) + chipset = ChipsetA(transport, logger=log) + device = DeviceA(chipset, logger=log) + device._vendor_name = "Arygon" + device._device_name = "ADRA" + return device + + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) diff --git a/src/lib/nfc/clf/device.py b/src/lib/nfc/clf/device.py new file mode 100644 index 0000000..69c0622 --- /dev/null +++ b/src/lib/nfc/clf/device.py @@ -0,0 +1,660 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""All contactless drivers must implement the interface defined in +:class:`~nfc.clf.device.Device`. Unsupported target discovery or target +emulation methods raise :exc:`~nfc.clf.UnsupportedTargetError`. The +interface is used internally by :class:`~nfc.clf.ContactlessFrontend` +and is not intended as an application programming interface. Device +driver methods are not thread-safe and do not necessarily check input +arguments when they are supposed to be valid. The interface may change +without notice at any time. + +""" +from . import transport + +import os +import sys +import errno +import importlib + +import logging +log = logging.getLogger(__name__) + +usb_device_map = { + (0x054c, 0x0193): "pn531", # PN531 (Sony VID/PID) + (0x04cc, 0x0531): "pn531", # PN531 (Philips VID/PID), SCM SCL3710 + (0x04cc, 0x2533): "pn533", # NXP PN533 demo board + (0x04e6, 0x5591): "pn533", # SCM SCL3711 + (0x04e6, 0x5593): "pn533", # SCM SCL3712 + (0x054c, 0x02e1): "rcs956", # Sony RC-S330/360/370 + (0x054c, 0x06c1): "rcs380", # Sony RC-S380 + (0x054c, 0x06c3): "rcs380", # Sony RC-S380 + (0x072f, 0x2200): "acr122", # ACS ACR122U +} + +tty_driver_list = ["arygon", "pn532"] + + +def connect(path): + """Connect to a local device identified by *path* and load the + appropriate device driver. The *path* argument is documented at + :meth:`nfc.clf.ContactlessFrontend.open`. The return value is + either a :class:`Device` instance or :const:`None`. Note that not + all drivers can be autodetected, specifically for serial devices + *path* must usually also specify the driver. + + """ + assert isinstance(path, str) and len(path) > 0 + + found = transport.USB.find(path) + if found is not None: + for vid, pid, bus, dev in found: + module = usb_device_map.get((vid, pid)) + if module is None: + continue + + log.debug("loading {mod} driver for usb:{vid:04x}:{pid:04x}" + .format(mod=module, vid=vid, pid=pid)) + + if sys.platform.startswith("linux"): + devnode = "/dev/bus/usb/%03d/%03d" % (int(bus), int(dev)) + if not os.access(devnode, os.R_OK | os.W_OK): + log.debug("access denied to " + devnode) + if len(path.split(':')) < 3: + continue + else: + raise IOError(errno.EACCES, os.strerror(errno.EACCES)) + + driver = importlib.import_module("nfc.clf." + module) + try: + device = driver.init(transport.USB(bus, dev)) + except IOError as error: + log.debug(error) + if len(path.split(':')) < 3: + continue + else: + raise error + + device._path = "usb:{0:03}:{1:03}".format(int(bus), int(dev)) + return device + + found = transport.TTY.find(path) + if found is not None: + devices = found[0] + drivers = [found[1]] if found[1] else tty_driver_list + globbed = found[2] or drivers is tty_driver_list + for drv in drivers: + for dev in devices: + log.debug("trying {0} on {1}".format(drv, dev)) + driver = importlib.import_module("nfc.clf." + drv) + tty = None + try: + tty = transport.TTY(dev) + device = driver.init(tty) + device._path = dev + return device + except IOError as error: + log.debug(error) + if tty is not None: + tty.close() + if not globbed: + raise + + if path.startswith("udp"): + path = path.split(':') + host = str(path[1]) if len(path) > 1 and path[1] else 'localhost' + port = int(path[2]) if len(path) > 2 and path[2] else 54321 + driver = importlib.import_module("nfc.clf.udp") + device = driver.init(host, port) + device._path = "udp:{0}:{1}".format(host, port) + return device + + +class Device(object): + """All device drivers inherit from the :class:`Device` class and must + implement it's methods. + + """ + def __init__(self, *args, **kwargs): + fname = "__init__" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def __str__(self): + strings = (self.vendor_name, self.product_name, self.chipset_name) + return ' '.join(filter(bool, strings)) + " at " + self.path + + @property + def vendor_name(self): + """The device vendor name. An empty string if the vendor name could + not be determined. + + """ + return self._vendor_name if hasattr(self, "_vendor_name") else '' + + @property + def product_name(self): + """The device product name. An empty string if the product name could + not be determined. + + """ + return self._device_name if hasattr(self, "_device_name") else '' + + @property + def chipset_name(self): + """The name of the chipset embedded in the device.""" + return self._chipset_name + + @property + def path(self): + return self._path + + def close(self): + fname = "close" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def mute(self): + """Mutes all existing communication, most notably the device will no + longer generate a 13.56 MHz carrier signal when operating as + Initiator. + + """ + fname = "mute" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def sense_tta(self, target): + """Discover a Type A Target. + + Activates the 13.56 MHz carrier signal and sends a SENS_REQ + command at the bitrate set by **target.brty**. If a response + is received, sends an RID_CMD for a Type 1 Tag or SDD_REQ and + SEL_REQ for a Type 2/4 Tag and returns the responses. + + Arguments: + + target (nfc.clf.RemoteTarget): Supplies bitrate and optional + command data for the target discovery. The only sensible + command to set is **sel_req** populated with a UID to find + only that specific target. + + Returns: + + nfc.clf.RemoteTarget: Response data received from a remote + target if found. This includes at least **sens_res** and + either **rid_res** (for a Type 1 Tag) or **sdd_res** and + **sel_res** (for a Type 2/4 Tag). + + Raises: + + nfc.clf.UnsupportedTargetError: The method is not supported + or the *target* argument requested an unsupported bitrate + (or has a wrong technology type identifier). + + """ + fname = "sense_tta" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def sense_ttb(self, target): + """Discover a Type B Target. + + Activates the 13.56 MHz carrier signal and sends a SENSB_REQ + command at the bitrate set by **target.brty**. If a SENSB_RES + is received, returns a target object with the **sensb_res** + attribute. + + Note that the firmware of some devices (least all those based + on PN53x) automatically sends an ATTRIB command with varying + but always unfortunate communication settings. The drivers + correct that situation by sending S(DESELECT) and WUPB before + return. + + Arguments: + + target (nfc.clf.RemoteTarget): Supplies bitrate and the + optional **sensb_req** for target discovery. Most drivers + do no not allow a fully customized SENSB_REQ, the only + parameter that can always be changed is the AFI byte, + others may be ignored. + + Returns: + + nfc.clf.RemoteTarget: Response data received from a remote + target if found. The only response data attribute is + **sensb_res**. + + Raises: + + nfc.clf.UnsupportedTargetError: The method is not supported + or the *target* argument requested an unsupported bitrate + (or has a wrong technology type identifier). + + """ + fname = "sense_ttb" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def sense_ttf(self, target): + """Discover a Type F Target. + + Activates the 13.56 MHz carrier signal and sends a SENSF_REQ + command at the bitrate set by **target.brty**. If a SENSF_RES + is received, returns a target object with the **sensf_res** + attribute. + + Arguments: + + target (nfc.clf.RemoteTarget): Supplies bitrate and the + optional **sensf_req** for target discovery. The default + SENSF_REQ invites all targets to respond and requests the + system code information bytes. + + Returns: + + nfc.clf.RemoteTarget: Response data received from a remote + target if found. The only response data attribute is + **sensf_res**. + + Raises: + + nfc.clf.UnsupportedTargetError: The method is not supported + or the *target* argument requested an unsupported bitrate + (or has a wrong technology type identifier). + + """ + fname = "sense_ttf" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def sense_dep(self, target): + """Discover a NFC-DEP Target in active communication mode. + + Activates the 13.56 MHz carrier signal and sends an ATR_REQ + command at the bitrate set by **target.brty**. If an ATR_RES + is received, returns a target object with the **atr_res** + attribute. + + Note that some drivers (like pn531) may modify the transport + data bytes length reduction value in ATR_REQ and ATR_RES due + to hardware limitations. + + Arguments: + + target (nfc.clf.RemoteTarget): Supplies bitrate and the + mandatory **atr_req** for target discovery. The bitrate + may be one of '106A', '212F', or '424F'. + + Returns: + + nfc.clf.RemoteTarget: Response data received from a remote + target if found. The only response data attribute is + **atr_res**. The actually sent and potentially modified + ATR_REQ is also included as **atr_req** attribute. + + Raises: + + nfc.clf.UnsupportedTargetError: The method is not supported + or the *target* argument requested an unsupported bitrate + (or has a wrong technology type identifier). + + """ + fname = "sense_dep" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def listen_tta(self, target, timeout): + """Listen as Type A Target. + + Waits to receive a SENS_REQ command at the bitrate set by + **target.brty** and sends the **target.sens_res** + response. Depending on the SENS_RES bytes, the Initiator then + sends an RID_CMD (SENS_RES coded for a Type 1 Tag) or SDD_REQ + and SEL_REQ (SENS_RES coded for a Type 2/4 Tag). Responses are + then generated from the **rid_res** or **sdd_res** and + **sel_res** attributes in *target*. + + Note that none of the currently supported hardware can + actually receive an RID_CMD, thus Type 1 Tag emulation is + impossible. + + Arguments: + + target (nfc.clf.LocalTarget): Supplies bitrate and mandatory + response data to reply when being discovered. + + timeout (float): The maximum number of seconds to wait for a + discovery command. + + Returns: + + nfc.clf.LocalTarget: Command data received from the remote + Initiator if being discovered and to the extent supported + by the device. The first command received after discovery + is returned as one of the **tt1_cmd**, **tt2_cmd** or + **tt4_cmd** attribute (note that unset attributes are + always None). + + Raises: + + nfc.clf.UnsupportedTargetError: The method is not supported + or the *target* argument requested an unsupported bitrate + (or has a wrong technology type identifier). + + ~exceptions.ValueError: A required target response attribute + is not present or does not supply the number of bytes + expected. + + """ + fname = "listen_tta" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def listen_ttb(self, target, timeout): + """Listen as Type A Target. + + Waits to receive a SENSB_REQ command at the bitrate set by + **target.brty** and sends the **target.sensb_res** + response. + + Note that none of the currently supported hardware can + actually listen as Type B target. + + Arguments: + + target (nfc.clf.LocalTarget): Supplies bitrate and mandatory + response data to reply when being discovered. + + timeout (float): The maximum number of seconds to wait for a + discovery command. + + Returns: + + nfc.clf.LocalTarget: Command data received from the remote + Initiator if being discovered and to the extent supported + by the device. The first command received after discovery + is returned as **tt4_cmd** attribute. + + Raises: + + nfc.clf.UnsupportedTargetError: The method is not supported + or the *target* argument requested an unsupported bitrate + (or has a wrong technology type identifier). + + ~exceptions.ValueError: A required target response attribute + is not present or does not supply the number of bytes + expected. + + """ + fname = "listen_ttb" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def listen_ttf(self, target, timeout): + """Listen as Type A Target. + + Waits to receive a SENSF_REQ command at the bitrate set by + **target.brty** and sends the **target.sensf_res** + response. Then waits for a first command that is not a + SENSF_REQ and returns this as the **tt3_cmd** attribute. + + Arguments: + + target (nfc.clf.LocalTarget): Supplies bitrate and mandatory + response data to reply when being discovered. + + timeout (float): The maximum number of seconds to wait for a + discovery command. + + Returns: + + nfc.clf.LocalTarget: Command data received from the remote + Initiator if being discovered and to the extent supported + by the device. The first command received after discovery + is returned as **tt3_cmd** attribute. + + Raises: + + nfc.clf.UnsupportedTargetError: The method is not supported + or the *target* argument requested an unsupported bitrate + (or has a wrong technology type identifier). + + ~exceptions.ValueError: A required target response attribute + is not present or does not supply the number of bytes + expected. + + """ + fname = "listen_ttf" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def listen_dep(self, target, timeout): + """Listen as NFC-DEP Target. + + Waits to receive an ATR_REQ (if the local device supports + active communication mode) or a Type A or F Target activation + followed by an ATR_REQ in passive communication mode. The + ATR_REQ is replied with **target.atr_res**. The first DEP_REQ + command is returned as the **dep_req** attribute along with + **atr_req** and **atr_res**. The **psl_req** and **psl_res** + attributes are returned when the has Initiator performed a + parameter selection. The **sens_res** or **sensf_res** + attributes are returned when activation was in passive + communication mode. + + Arguments: + + target (nfc.clf.LocalTarget): Supplies mandatory response + data to reply when being discovered. All of **sens_res**, + **sdd_res**, **sel_res**, **sensf_res**, and **atr_res** + must be provided. The bitrate does not need to be set, an + NFC-DEP Target always accepts discovery at '106A', '212F + and '424F'. + + timeout (float): The maximum number of seconds to wait for a + discovery command. + + Returns: + + nfc.clf.LocalTarget: Command data received from the remote + Initiator if being discovered and to the extent supported + by the device. The first command received after discovery + is returned as **dep_req** attribute. + + Raises: + + nfc.clf.UnsupportedTargetError: The method is not supported + by the local hardware. + + ~exceptions.ValueError: A required target response attribute + is not present or does not supply the number of bytes + expected. + + """ + fname = "listen_dep" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def send_cmd_recv_rsp(self, target, data, timeout): + """Exchange data with a remote Target + + Sends command *data* to the remote *target* discovered in the + most recent call to one of the sense_xxx() methods. Note that + *target* becomes invalid with any call to mute(), sense_xxx() + or listen_xxx() + + Arguments: + + target (nfc.clf.RemoteTarget): The target returned by the + last successful call of a sense_xxx() method. + + data (bytearray): The binary data to send to the remote + device. + + timeout (float): The maximum number of seconds to wait for + response data from the remote device. + + Returns: + + bytearray: Response data received from the remote device. + + Raises: + + nfc.clf.CommunicationError: When no data was received. + + """ + fname = "send_cmd_recv_rsp" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def send_rsp_recv_cmd(self, target, data, timeout=None): + """Exchange data with a remote Initiator + + Sends response *data* as the local *target* being discovered + in the most recent call to one of the listen_xxx() methods. + Note that *target* becomes invalid with any call to mute(), + sense_xxx() or listen_xxx() + + Arguments: + + target (nfc.clf.LocalTarget): The target returned by the + last successful call of a listen_xxx() method. + + data (bytearray): The binary data to send to the remote + device. + + timeout (float): The maximum number of seconds to wait for + command data from the remote device. + + Returns: + + bytearray: Command data received from the remote device. + + Raises: + + nfc.clf.CommunicationError: When no data was received. + + """ + fname = "send_rsp_recv_cmd" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def get_max_send_data_size(self, target): + """Returns the maximum number of data bytes for sending. + + The maximum number of data bytes acceptable for sending with + either :meth:`send_cmd_recv_rsp` or :meth:`send_rsp_recv_cmd`. + The value reflects the local device capabilities for sending + in the mode determined by *target*. It does not relate to any + protocol capabilities and negotiations. + + Arguments: + + target (nfc.clf.Target): The current local or remote + communication target. + + Returns: + + int: Maximum number of data bytes supported for sending. + + """ + fname = "get_max_send_data_size" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def get_max_recv_data_size(self, target): + """Returns the maximum number of data bytes for receiving. + + The maximum number of data bytes acceptable for receiving with + either :meth:`send_cmd_recv_rsp` or :meth:`send_rsp_recv_cmd`. + The value reflects the local device capabilities for receiving + in the mode determined by *target*. It does not relate to any + protocol capabilities and negotiations. + + Arguments: + + target (nfc.clf.Target): The current local or remote + communication target. + + Returns: + + int: Maximum number of data bytes supported for receiving. + + """ + fname = "get_max_recv_data_size" + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError("%s.%s() is required" % (cname, fname)) + + def turn_on_led_and_buzzer(self): + """If a device has an LED and/or a buzzer, this method can be + implemented to turn those indicators to the ON state. + + """ + pass + + def turn_off_led_and_buzzer(self): + """If a device has an LED and/or a buzzer, this method can be + implemented to turn those indicators to the OFF state. + + """ + pass + + @staticmethod + def add_crc_a(data): + # Calculate CRC-A for bytearray *data* and return *data* + # extended with the two CRC bytes. + crc = calculate_crc(data, len(data), 0x6363) + return data + bytearray([crc & 0xff, crc >> 8]) + + @staticmethod + def check_crc_a(data): + # Calculate CRC-A for the leading *len(data)-2* bytes of + # bytearray *data* and return whether the result matches the + # trailing 2 bytes of *data*. + crc = calculate_crc(data, len(data)-2, 0x6363) + return (data[-2], data[-1]) == (crc & 0xff, crc >> 8) + + @staticmethod + def add_crc_b(data): + # Calculate CRC-B for bytearray *data* and return *data* + # extended with the two CRC bytes. + crc = ~calculate_crc(data, len(data), 0xFFFF) & 0xFFFF + return data + bytearray([crc & 0xff, crc >> 8]) + + @staticmethod + def check_crc_b(data): + # Calculate CRC-B for the leading *len(data)-2* bytes of + # bytearray *data* and return whether the result matches the + # trailing 2 bytes of *data*. + crc = ~calculate_crc(data, len(data)-2, 0xFFFF) & 0xFFFF + return (data[-2], data[-1]) == (crc & 0xff, crc >> 8) + + +def calculate_crc(data, size, reg): + for octet in data[:size]: + for pos in range(8): + bit = (reg ^ ((octet >> pos) & 1)) & 1 + reg = reg >> 1 + if bit: + reg = reg ^ 0x8408 + return reg diff --git a/src/lib/nfc/clf/pn531.py b/src/lib/nfc/clf/pn531.py new file mode 100644 index 0000000..8153922 --- /dev/null +++ b/src/lib/nfc/clf/pn531.py @@ -0,0 +1,316 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""Driver module for contactless devices based on the NXP PN531 +chipset. This was once a (sort of) joint development between Philips +and Sony to supply hardware capable of running the ISO/IEC 18092 Data +Exchange Protocol. The chip has selectable UART, I2C, SPI, or USB host +interfaces, For USB the vendor and product ID can be switched by a +hardware pin to either Philips or Sony. + +The internal chipset architecture comprises a small 8-bit MCU and a +Contactless Interface Unit CIU that is basically a PN511. The CIU +implements the analog and digital part of communication (modulation +and framing) while the MCU handles the protocol parts and host +communication. The PN511 and hence the PN531 does not support Type B +Technology and can not handle the specific Jewel/Topaz (Type 1 Tag) +communication. Compared to PN532/PN533 the host frame structure does +not allow maximum size ISO/IEC 18092 packets to be transferred. The +driver handles this restriction by modifying the initialization +commands (ATR, PSL) when needed. + +========== ======= ============ +function support remarks +========== ======= ============ +sense_tta yes Type 1 Tag is not supported +sense_ttb no +sense_ttf yes +sense_dep yes Reduced transport data byte length (max 192) +listen_tta yes +listen_ttb no +listen_ttf yes Maximimum frame size is 64 byte +listen_dep yes +========== ======= ============ + +""" +import nfc.clf +from . import pn53x + +import logging +log = logging.getLogger(__name__) + + +class Chipset(pn53x.Chipset): + CMD = { + # Miscellaneous + 0x00: "Diagnose", + 0x02: "GetFirmwareVersion", + 0x04: "GetGeneralStatus", + 0x06: "ReadRegister", + 0x08: "WriteRegister", + 0x0C: "ReadGPIO", + 0x0E: "WriteGPIO", + 0x10: "SetSerialBaudrate", + 0x12: "SetTAMAParameters", + 0x14: "SAMConfiguration", + 0x16: "PowerDown", + # RF communication + 0x32: "RFConfiguration", + 0x58: "RFRegulationTest", + # Initiator + 0x56: "InJumpForDEP", + 0x46: "InJumpForPSL", + 0x4A: "InListPassiveTarget", + 0x50: "InATR", + 0x4E: "InPSL", + 0x40: "InDataExchange", + 0x42: "InCommunicateThru", + 0x44: "InDeselect", + 0x52: "InRelease", + 0x54: "InSelect", + # Target + 0x8C: "TgInitTAMATarget", + 0x92: "TgSetGeneralBytes", + 0x86: "TgGetDEPData", + 0x8E: "TgSetDEPData", + 0x94: "TgSetMetaDEPData", + 0x88: "TgGetInitiatorCommand", + 0x90: "TgResponseToInitiator", + 0x8A: "TgGetTargetStatus", + } + ERR = { + 0x01: "Time out, the Target has not answered", + 0x02: "Checksum error during RF communication", + 0x03: "Parity error during RF communication", + 0x04: "Erroneous bit count in anticollision", + 0x05: "Framing error during Mifare operation", + 0x06: "Abnormal bit collision in 106 kbps anticollision", + 0x07: "Insufficient communication buffer size", + 0x09: "RF buffer overflow detected by CIU", + 0x0a: "RF field not activated in time by active mode peer", + 0x0b: "Protocol error during RF communication", + 0x0d: "Overheated - antenna drivers deactivated", + 0x0e: "Internal buffer overflow", + 0x10: "Invalid command parameter", + 0x12: "Unsupported command from Initiator", + 0x13: "Format error during RF communication", + 0x14: "Mifare authentication error", + 0x23: "ISO/IEC14443-3 UID check byte is wrong", + 0x25: "Command invalid in current DEP state", + 0x26: "Operation not allowed in this configuration", + 0x27: "Command is not acceptable in the current context", + 0x7f: "Invalid command syntax - received error frame", + 0xff: "Insufficient data received from executing chip command", + } + + host_command_frame_max_size = 254 + """Maximum host command frame size.""" + + in_list_passive_target_max_target = 2 + """Maximum number of targets for the InListPassiveTarget command.""" + + in_list_passive_target_brty_range = (0, 1, 2) + """Possible values for the brty parameter to InListPassiveTarget.""" + + def _read_register(self, data): + return self.command(0x06, data, timeout=0.25) + + def _write_register(self, data): + self.command(0x08, data, timeout=0.25) + + sam_configuration_modes = ("normal", "virtual", "wired", "dual") + """Possible SAM configuration modes.""" + + def sam_configuration(self, mode, timeout=0): + """Send the SAMConfiguration command to configure the Security Access + Module. The *mode* argument must be one of the string values + in :data:`sam_configuration_modes`. The *timeout* argument is + only relevant for the virtual card configuration mode. + + """ + mode = self.sam_configuration_modes.index(mode) + 1 + self.command(0x14, bytearray([mode, timeout]), timeout=0.1) + + power_down_wakeup_sources = ("INT0", "INT1", "USB", "RF", "HSU", "SPI") + """Possible wake up sources for the :meth:`power_down` method.""" + + def power_down(self, wakeup_enable): + """Send the PowerDown command to put the PN531 (including the + contactless analog front end) into power down mode in order to + save power consumption. The *wakeup_enable* argument must be a + list of wake up sources with values from the + :data:`power_down_wakeup_sources`. + + """ + wakeup_set = 0 + for i, src in enumerate(self.power_down_wakeup_sources): + if src in wakeup_enable: + wakeup_set |= 1 << i + data = self.command(0x16, bytearray([wakeup_set]), timeout=0.1) + if data[0] != 0: + self.chipset_error(data) + + def tg_init_tama_target(self, mode, mifare_params, felica_params, + nfcid3t, gt, timeout): + """Send the TgInitTAMATarget command.""" + assert type(mode) is int and mode & 0b11111100 == 0 + assert len(mifare_params) == 6 + assert len(felica_params) == 18 + assert len(nfcid3t) == 10 + + data = bytearray([mode]) + mifare_params + felica_params + nfcid3t + gt + return self.command(0x8c, data, timeout) + + +class Device(pn53x.Device): + # Device driver for PN531 based contactless frontends. + + def __init__(self, chipset, logger): + assert isinstance(chipset, Chipset) + super(Device, self).__init__(chipset, logger) + + ver, rev = self.chipset.get_firmware_version() + self._chipset_name = "PN531v{0}.{1}".format(ver, rev) + self.log.debug("chipset is a {0}".format(self._chipset_name)) + + self.chipset.sam_configuration("normal") + self.chipset.set_parameters(0b00000000) + self.chipset.rf_configuration(0x02, b"\x00\x0B\x0A") + self.chipset.rf_configuration(0x04, b"\x00") + self.chipset.rf_configuration(0x05, b"\x01\x00\x01") + self.mute() + + def close(self): + self.mute() + super(Device, self).close() + + def sense_tta(self, target): + """Activate the RF field and probe for a Type A Target. + + The PN531 can discover some Type A Targets (Type 2 Tag and + Type 4A Tag) at 106 kbps. Type 1 Tags (Jewel/Topaz) are + completely unsupported. Because the firmware does not evaluate + the SENS_RES before sending SDD_REQ, it may be that a warning + message about missing Type 1 Tag support is logged even if a + Type 2 or 4A Tag was present. This typically happens when the + SDD_RES or SEL_RES are lost due to communication errors + (normally when the tag is moved away). + + """ + target = super(Device, self).sense_tta(target) + if target and target.sdd_res and len(target.sdd_res) > 4: + # Remove the cascade tag(s) from SDD_RES, only the PN531 + # has them included and we've set the policy that cascade + # tags are not part of the sel_req/sdd_res parameters. + if len(target.sdd_res) == 8: + target.sdd_res = target.sdd_res[1:] + elif len(target.sdd_res) == 12: + target.sdd_res = target.sdd_res[1:4] + target.sdd_res[5:] + # Also the SENS_RES bytes are reversed compared to PN532/533 + target.sens_res = bytearray(reversed(target.sens_res)) + return target + + def sense_ttb(self, target): + """Sense for a Type B Target is not supported.""" + info = "{device} does not support sense for Type B Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def sense_ttf(self, target): + """Activate the RF field and probe for a Type F Target. + + """ + return super(Device, self).sense_ttf(target) + + def sense_dep(self, target): + """Search for a DEP Target in active communication mode. + + Because the PN531 does not implement the extended frame syntax + for host controller communication, it can not support the + maximum payload size of 254 byte. The driver handles this by + modifying the length-reduction values in atr_req and atr_res. + + """ + if target.atr_req[15] & 0x30 == 0x30: + self.log.warning("must reduce the max payload size in atr_req") + target.atr_req[15] = (target.atr_req[15] & 0xCF) | 0x20 + + target = super(Device, self).sense_dep(target) + if target is None: + return + + if target.atr_res[16] & 0x30 == 0x30: + self.log.warning("must reduce the max payload size in atr_res") + atr_res = bytearray(target.atr_res) + atr_res[16] = (target.atr_res[16] & 0xCF) | 0x20 + target.atr_res = bytes(atr_res) + + return target + + def listen_tta(self, target, timeout): + """Listen *timeout* seconds for a Type A activation at 106 kbps. The + ``sens_res``, ``sdd_res``, and ``sel_res`` response data must + be provided and ``sdd_res`` must be a 4 byte UID that starts + with ``08h``. Depending on ``sel_res`` an activation may + return a target with a ``tt2_cmd``, ``tt4_cmd`` or ``atr_req`` + attribute. The default RATS response sent for a Type 4 Tag + activation can be replaced with a ``rats_res`` attribute. + + """ + return super(Device, self).listen_tta(target, timeout) + + def listen_ttb(self, target, timeout): + """Listen as Type B Target is not supported.""" + info = "{device} does not support listen as Type B Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_ttf(self, target, timeout): + """Listen *timeout* seconds for a Type F card activation. The target + ``brty`` must be set to either 212F or 424F and ``sensf_res`` + provide 19 byte response data (response code + 8 byte IDm + 8 + byte PMm + 2 byte system code). Note that the maximum command + an response frame length is 64 bytes only (including the frame + length byte), because the driver must directly program the + contactless interface unit within the PN533. + + """ + return super(Device, self).listen_ttf(target, timeout) + + def listen_dep(self, target, timeout): + """Listen *timeout* seconds to become initialized as a DEP Target. + + The PN531 can be set to listen as a DEP Target for passive and + active communication mode. + + """ + return super(Device, self).listen_dep(target, timeout) + + def _init_as_target(self, mode, tta_params, ttf_params, timeout): + nfcid3t = ttf_params[0:8] + b"\x00\x00" + args = (mode, tta_params, ttf_params, nfcid3t, b'', timeout) + return self.chipset.tg_init_tama_target(*args) + + +def init(transport): + chipset = Chipset(transport, logger=log) + device = Device(chipset, logger=log) + device._vendor_name = transport.manufacturer_name + device._device_name = transport.product_name + return device diff --git a/src/lib/nfc/clf/pn532.py b/src/lib/nfc/clf/pn532.py new file mode 100644 index 0000000..5ef8d91 --- /dev/null +++ b/src/lib/nfc/clf/pn532.py @@ -0,0 +1,454 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""Driver module for contactless devices based on the NXP PN532 +chipset. This successor of the PN531 can additionally handle Type B +Technology (type 4B Tags) and Type 1 Tag communication. It also +supports an extended frame syntax for host communication that allows +larger packets to be transferred. The chip has selectable UART, I2C or +SPI host interfaces. A speciality of the PN532 is that it can manage +two targets (cards) simultanously, although this is not used by +*nfcpy*. + +The internal chipset architecture comprises a small 8-bit MCU and a +Contactless Interface Unit CIU that is basically a PN512. The CIU +implements the analog and digital part of communication (modulation +and framing) while the MCU handles the protocol parts and host +communication. Almost all PN532 firmware limitations (or bugs) can be +avoided by directly programming the CIU. Type F Target mode for card +emulation is completely implemented with the CIU and limited to 64 +byte frame exchanges by the CIU's FIFO size. Type B Target mode is not +possible. + +========== ======= ============ +function support remarks +========== ======= ============ +sense_tta yes +sense_ttb yes +sense_ttf yes +sense_dep yes +listen_tta yes +listen_ttb no +listen_ttf yes Maximimum frame size is 64 byte +listen_dep yes +========== ======= ============ + +""" +import nfc.clf +from . import pn53x + +import os +import sys +import time +import errno + +import logging +log = logging.getLogger(__name__) + + +class Chipset(pn53x.Chipset): + CMD = { + # Miscellaneous + 0x00: "Diagnose", + 0x02: "GetFirmwareVersion", + 0x04: "GetGeneralStatus", + 0x06: "ReadRegister", + 0x08: "WriteRegister", + 0x0C: "ReadGPIO", + 0x0E: "WriteGPIO", + 0x10: "SetSerialBaudrate", + 0x12: "SetParameters", + 0x14: "SAMConfiguration", + 0x16: "PowerDown", + # RF communication + 0x32: "RFConfiguration", + 0x58: "RFRegulationTest", + # Initiator + 0x56: "InJumpForDEP", + 0x46: "InJumpForPSL", + 0x4A: "InListPassiveTarget", + 0x50: "InATR", + 0x4E: "InPSL", + 0x40: "InDataExchange", + 0x42: "InCommunicateThru", + 0x44: "InDeselect", + 0x52: "InRelease", + 0x54: "InSelect", + 0x60: "InAutoPoll", + # Target + 0x8C: "TgInitAsTarget", + 0x92: "TgSetGeneralBytes", + 0x86: "TgGetData", + 0x8E: "TgSetData", + 0x94: "TgSetMetaData", + 0x88: "TgGetInitiatorCommand", + 0x90: "TgResponseToInitiator", + 0x8A: "TgGetTargetStatus", + } + ERR = { + 0x01: "Time out, the Target has not answered", + 0x02: "Checksum error during RF communication", + 0x03: "Parity error during RF communication", + 0x04: "Erroneous bit count in anticollision", + 0x05: "Framing error during Mifare operation", + 0x06: "Abnormal bit collision in 106 kbps anticollision", + 0x07: "Insufficient communication buffer size", + 0x09: "RF buffer overflow detected by CIU", + 0x0a: "RF field not activated in time by active mode peer", + 0x0b: "Protocol error during RF communication", + 0x0d: "Overheated - antenna drivers deactivated", + 0x0e: "Internal buffer overflow", + 0x10: "Invalid command parameter", + 0x12: "Unsupported command from Initiator", + 0x13: "Format error during RF communication", + 0x14: "Mifare authentication error", + 0x23: "ISO/IEC14443-3 UID check byte is wrong", + 0x25: "Command invalid in current DEP state", + 0x26: "Operation not allowed in this configuration", + 0x27: "Command is not acceptable in the current context", + 0x29: "Released by Initiator while operating as Target", + 0x2A: "ISO/IEC14443-3B, the ID of the card does not match", + 0x2B: "ISO/IEC14443-3B, card previously activated has disappeared", + 0x2C: "NFCID3i and NFCID3t mismatch in DEP 212/424 kbps passive", + 0x2D: "An over-current event has been detected", + 0x2E: "NAD missing in DEP frame", + 0x7f: "Invalid command syntax - received error frame", + 0xff: "Insufficient data received from executing chip command", + } + + host_command_frame_max_size = 265 + in_list_passive_target_max_target = 2 + in_list_passive_target_brty_range = (0, 1, 2, 3, 4) + + def _read_register(self, data): + return self.command(0x06, data, timeout=0.25) + + def _write_register(self, data): + self.command(0x08, data, timeout=0.25) + + def set_serial_baudrate(self, baudrate): + br = (9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 1288000) + + self.command(0x10, bytearray([br.index(baudrate)]), timeout=0.1) + self.write_frame(self.ACK) + time.sleep(0.001) + + def sam_configuration(self, mode, timeout=0, irq=False): + mode = ("normal", "virtual", "wired", "dual").index(mode) + 1 + self.command(0x14, bytearray([mode, timeout, int(irq)]), timeout=0.1) + + power_down_wakeup_src = ("INT0", "INT1", "rfu", "RF", + "HSU", "SPI", "GPIO", "I2C") + + def power_down(self, wakeup_enable, generate_irq=False): + wakeup_set = 0 + for i, src in enumerate(self.power_down_wakeup_src): + if src in wakeup_enable: + wakeup_set |= 1 << i + cmd_data = bytearray([wakeup_set, int(generate_irq)]) + data = self.command(0x16, cmd_data, timeout=0.1) + if data[0] != 0: + self.chipset_error(data) + + def tg_init_as_target(self, mode, mifare_params, felica_params, nfcid3t, + general_bytes=b'', historical_bytes=b'', + timeout=None): + assert type(mode) is int and mode & 0b11111000 == 0 + assert len(mifare_params) == 6 + assert len(felica_params) == 18 + assert len(nfcid3t) == 10 + + data = (bytearray([mode]) + mifare_params + felica_params + nfcid3t + + bytearray([len(general_bytes)]) + general_bytes + + bytearray([len(historical_bytes)]) + historical_bytes) + return self.command(0x8c, data, timeout) + + +class Device(pn53x.Device): + # Device driver for PN532 based contactless frontends. + + def __init__(self, chipset, logger): + assert isinstance(chipset, Chipset) + super(Device, self).__init__(chipset, logger) + + ic, ver, rev, support = self.chipset.get_firmware_version() + self._chipset_name = "PN5{0:02x}v{1}.{2}".format(ic, ver, rev) + self.log.debug("chipset is a {0}".format(self._chipset_name)) + + self.chipset.set_parameters(0b00000000) + self.chipset.rf_configuration(0x02, b"\x00\x0B\x0A") + self.chipset.rf_configuration(0x04, b"\x00") + self.chipset.rf_configuration(0x05, b"\x01\x00\x01") + + self.log.debug("write analog settings for Type A 106 kbps") + data = bytearray.fromhex("59 F4 3F 11 4D 85 61 6F 26 62 87") + self.chipset.rf_configuration(0x0A, data) + + self.log.debug("write analog settings for Type F 212/424 kbps") + data = bytearray.fromhex("69 FF 3F 11 41 85 61 6F") + self.chipset.rf_configuration(0x0B, data) + + self.log.debug("write analog settings for Type B 106 kbps") + data = bytearray.fromhex("FF 04 85") + self.chipset.rf_configuration(0x0C, data) + + self.log.debug("write analog settings for 14443-4 212/424/848 kbps") + data = bytearray.fromhex("85 15 8A 85 08 B2 85 01 DA") + self.chipset.rf_configuration(0x0D, data) + + self.mute() + + def close(self): + # Cancel most recent command in case we've been interrupted + # before the response, give the chip 10 ms to think about it. + self.chipset.send_ack() + time.sleep(0.01) + + # When using the high speed uart we must set the baud rate + # back to 115.2 kbps, otherwise we can't talk next time. + if self.chipset.transport.TYPE == "TTY": + self.chipset.set_serial_baudrate(115200) + self.chipset.transport.baudrate = 115200 + + # Set the chip to sleep mode with some wakeup sources. + self.chipset.power_down(wakeup_enable=("I2C", "SPI", "HSU")) + super(Device, self).close() + + def sense_tta(self, target): + """Search for a Type A Target. + + The PN532 can discover all kinds of Type A Targets (Type 1 + Tag, Type 2 Tag, and Type 4A Tag) at 106 kbps. + + """ + return super(Device, self).sense_tta(target) + + def sense_ttb(self, target): + """Search for a Type B Target. + + The PN532 can discover Type B Targets (Type 4B Tag) at 106 + kbps. For a Type 4B Tag the firmware automatically sends an + ATTRIB command that configures the use of DID and 64 byte + maximum frame size. The driver reverts this configuration with + a DESELECT and WUPB command to return the target prepared for + activation (which nfcpy does in the tag activation code). + + """ + return super(Device, self).sense_ttb(target, did=b'\x01') + + def sense_ttf(self, target): + """Search for a Type F Target. + + The PN532 can discover Type F Targets (Type 3 Tag) at 212 and + 424 kbps. The driver uses the default polling command + ``06FFFF0000`` if no ``target.sens_req`` is supplied. + + """ + return super(Device, self).sense_ttf(target) + + def sense_dep(self, target): + """Search for a DEP Target in active communication mode.""" + return super(Device, self).sense_dep(target) + + def _tt1_send_cmd_recv_rsp(self, data, timeout): + # Special handling for Tag Type 1 (Jewel/Topaz) card commands. + + if data[0] in (0x00, 0x01, 0x1A, 0x53, 0x72): + # These commands are implemented by the chipset. + return self.chipset.in_data_exchange(data, timeout)[0] + + if data[0] == 0x10: + # RSEG implementation does not accept any segment other + # than 0. Unfortunately we can not directly issue this + # command to the CIU because the response is 128 byte and + # we're not fast enough to read it from the 64 byte FIFO. + rsp = data[1:2] + for block in range((data[1] >> 4) * 16, (data[1] >> 4) * 16 + 16): + cmd = bytearray([0x02, block]) + data[2:] + rsp += self._tt1_send_cmd_recv_rsp(cmd, timeout)[1:9] + return rsp + + # Remaining commands READ8, WRITE-E8, WRITE-NE8 are not + # implemented by the chipset. Fortunately we can directly + # program the CIU through register read/write. Each TT1 + # command byte must be send as a separate Type A frame, the + # first as a short frame with only 7 data bits and the others + # as normal frames. Reading is also a bit complicated because + # for sending we have to disable the parity generator which + # means that we will also receive the parity bits, thus 9 bits + # received per 8 data bits. And because they are already + # reversed in the FIFO we must swap before parity removal and + # afterwards (maybe this could be optimized a bit) + data = self.add_crc_b(data) + register_write = [] + register_write.append(("CIU_FIFOData", data[0])) # CMD_CODE + register_write.append(("CIU_BitFraming", 0x07)) # 7 bits + register_write.append(("CIU_Command", 0x04)) # Transmit + register_write.append(("CIU_BitFraming", 0x00)) # 8 bits + register_write.append(("CIU_ManualRCV", 0x30)) # ParityDisable + for i in range(1, len(data)): + register_write.append(("CIU_FIFOData", data[i])) # CMD_DATA + register_write.append(("CIU_Command", 0x04)) # Transmit + register_write.append(("CIU_Command", 0x07)) # NoCmdChange + register_write.append(("CIU_Command", 0x08)) # Receive + self.chipset.write_register(*register_write) + if data[0] == 0x54: # WRITE-E8 + time.sleep(0.006) # assuming same response time as WRITE-E + if data[0] == 0x1B: # WRITE-NE8 + time.sleep(0.003) # assuming same response time as WRITE-NE + self.chipset.write_register(("CIU_ManualRCV", 0x20)) # enable parity + fifo_level = self.chipset.read_register("CIU_FIFOLevel") + if fifo_level == 0: + raise nfc.clf.TimeoutError + data = self.chipset.read_register(*(fifo_level * ["CIU_FIFOData"])) + data = ''.join(["{:08b}".format(octet)[::-1] for octet in data]) + data = [int(data[i:i+8][::-1], 2) for i in range(0, len(data)-8, 9)] + if self.check_crc_b(data) is False: + raise nfc.clf.TransmissionError("crc_b check error") + return bytearray(data[0:-2]) + + def listen_tta(self, target, timeout): + """Listen *timeout* seconds for a Type A activation at 106 kbps. The + ``sens_res``, ``sdd_res``, and ``sel_res`` response data must + be provided and ``sdd_res`` must be a 4 byte UID that starts + with ``08h``. Depending on ``sel_res`` an activation may + return a target with a ``tt2_cmd``, ``tt4_cmd`` or ``atr_req`` + attribute. The default RATS response sent for a Type 4 Tag + activation can be replaced with a ``rats_res`` attribute. + + """ + return super(Device, self).listen_tta(target, timeout) + + def listen_ttb(self, target, timeout): + """Listen as Type B Target is not supported.""" + info = "{device} does not support listen as Type B Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_ttf(self, target, timeout): + """Listen *timeout* seconds for a Type F card activation. The target + ``brty`` must be set to either 212F or 424F and ``sensf_res`` + provide 19 byte response data (response code + 8 byte IDm + 8 + byte PMm + 2 byte system code). Note that the maximum command + an response frame length is 64 bytes only (including the frame + length byte), because the driver must directly program the + contactless interface unit within the PN533. + + """ + return super(Device, self).listen_ttf(target, timeout) + + def listen_dep(self, target, timeout): + """Listen *timeout* seconds to become initialized as a DEP Target. + + The PN532 can be set to listen as a DEP Target for passive and + active communication mode. + + """ + return super(Device, self).listen_dep(target, timeout) + + def _init_as_target(self, mode, tta_params, ttf_params, timeout): + nfcid3t = ttf_params[0:8] + b"\x00\x00" + args = (mode, tta_params, ttf_params, nfcid3t, b'', b'', timeout) + return self.chipset.tg_init_as_target(*args) + + +def init(transport): + if transport.TYPE == "TTY": + baudrate = 115200 # PN532 initial baudrate + transport.open(transport.port, baudrate) + long_preamble = bytearray(10) + + # The PN532 chip should send an ack within 15 ms after a + # command. We'll give it a bit more and wait 100 ms, unless + # we're on a Raspberry Pi detected by the Broadcom SOC. The + # USB on BCM270x has a nasty bug (may be SW or HW) that + # introduces additional up to ~1000 ms delay for the first + # data from a ttyUSB. Tested with two serial converters + # (PL2303 and FT232R) in loopback and it's reproducable adding + # up to 1000 ms if a serial open is done 1 sec after serial + # close. Waiting longer decreases that time until after 2 sec + # wait between close and open it all goes fine until the wait + # time reaches 3 seconds, and so on. + initial_timeout = 100 # milliseconds + # change_baudrate = True # try higher speeds + change_baudrate = False # MOD GG *DO NOT* try higher speeds + if sys.platform.startswith('linux'): + board = b"" # Raspi board will identify through device tree + try: + board = open('/proc/device-tree/model', "rb").read().strip( + b'\x00') + except IOError: + pass + if board.startswith(b"Raspberry Pi"): + log.debug("running on {}".format(board)) + if transport.port.startswith("/dev/ttyUSB"): + log.debug("ttyUSB requires more time for first ack") + initial_timeout = 1500 # milliseconds + elif transport.port == "/dev/ttyS0": + log.debug("ttyS0 can only do 115.2 kbps") + change_baudrate = False # RPi 'mini uart' + + get_version_cmd = bytearray.fromhex("0000ff02fed4022a00") + get_version_rsp = bytearray.fromhex("0000ff06fad50332") + transport.write(long_preamble + get_version_cmd) + log.debug("wait %d ms for data on %s", initial_timeout, transport.port) + if not transport.read(timeout=initial_timeout) == Chipset.ACK: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + if not transport.read(timeout=100).startswith(get_version_rsp): + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + sam_configuration_cmd = bytearray.fromhex("0000ff05fbd4140100001700") + sam_configuration_rsp = bytearray.fromhex("0000ff02fed5151600") + transport.write(long_preamble + sam_configuration_cmd) + if not transport.read(timeout=100) == Chipset.ACK: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + if not transport.read(timeout=100) == sam_configuration_rsp: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + if sys.platform.startswith("linux") and change_baudrate is True: + stty = 'stty -F %s %%d 2> /dev/null' % transport.port + # MOD GG FIXED BAUD RATE + # for baudrate in (921600, 460800, 230400, 115200): + for baudrate in (115200,): + log.debug("trying to set %d baud", baudrate) + if os.system(stty % baudrate) == 0: + os.system(stty % 115200) + break + + if baudrate > 115200: + set_baudrate_cmd = bytearray.fromhex("0000ff03fdd410000000") + set_baudrate_rsp = bytearray.fromhex("0000ff02fed5111a00") + set_baudrate_cmd[7] = 5 + (230400, 460800, 921600).index(baudrate) + set_baudrate_cmd[8] = 256 - sum(set_baudrate_cmd[5:8]) + transport.write(long_preamble + set_baudrate_cmd) + if not transport.read(timeout=100) == Chipset.ACK: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + if not transport.read(timeout=100) == set_baudrate_rsp: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + transport.write(Chipset.ACK) + transport.open(transport.port, baudrate) + log.debug("changed uart speed to %d baud", baudrate) + time.sleep(0.001) + + chipset = Chipset(transport, logger=log) + return Device(chipset, logger=log) + + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) diff --git a/src/lib/nfc/clf/pn533.py b/src/lib/nfc/clf/pn533.py new file mode 100644 index 0000000..d17fc4f --- /dev/null +++ b/src/lib/nfc/clf/pn533.py @@ -0,0 +1,399 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""Driver module for contactless devices based on the NXP PN533 +chipset. The PN533 is pretty similar to the PN532 except that it also +has a USB host interface option and, probably due to the resources +needed for USB, does not support two simultaneous targets. Anything +else said about PN532 also applies to PN533. + +========== ======= ============ +function support remarks +========== ======= ============ +sense_tta yes +sense_ttb yes +sense_ttf yes +sense_dep yes +listen_tta yes +listen_ttb no +listen_ttf yes Maximimum frame size is 64 byte +listen_dep yes +========== ======= ============ + +""" +import nfc.clf +from . import pn53x + +import time + +import logging +log = logging.getLogger(__name__) + + +class Chipset(pn53x.Chipset): + CMD = { + # Miscellaneous + 0x00: "Diagnose", + 0x02: "GetFirmwareVersion", + 0x04: "GetGeneralStatus", + 0x06: "ReadRegister", + 0x08: "WriteRegister", + 0x0C: "ReadGPIO", + 0x0E: "WriteGPIO", + 0x12: "SetParameters", + 0x18: "AlparCommandForTDA", + # RF Communication + 0x32: "RFConfiguration", + 0x58: "RFRegulationTest", + # Initiator + 0x56: "InJumpForDEP", + 0x46: "InJumpForPSL", + 0x4A: "InListPassiveTarget", + 0x50: "InATR", + 0x4E: "InPSL", + 0x40: "InDataExchange", + 0x42: "InCommunicateThru", + 0x38: "InQuartetByteExchange", + 0x44: "InDeselect", + 0x52: "InRelease", + 0x54: "InSelect", + 0x48: "InActivateDeactivatePaypass", + # Target + 0x8C: "TgInitAsTarget", + 0x92: "TgSetGeneralBytes", + 0x86: "TgGetData", + 0x8E: "TgSetData", + 0x96: "TgSetDataSecure", + 0x94: "TgSetMetaData", + 0x98: "TgSetMetaDataSecure", + 0x88: "TgGetInitiatorCommand", + 0x90: "TgResponseToInitiator", + 0x8A: "TgGetTargetStatus", + } + ERR = { + 0x01: "Time out, the Target has not answered", + 0x02: "Checksum error during RF communication", + 0x03: "Parity error during RF communication", + 0x04: "Erroneous bit count in anticollision", + 0x05: "Framing error during mifare operation", + 0x06: "Abnormal bit collision in 106 kbps anticollision", + 0x07: "Insufficient communication buffer size", + 0x09: "RF buffer overflow detected by CIU", + 0x0a: "RF field not activated in time by active mode peer", + 0x0b: "Protocol error during RF communication", + 0x0d: "Overheated - antenna drivers deactivated", + 0x0e: "Internal buffer overflow", + 0x10: "Invalid command parameter", + 0x12: "Unsupported command from Initiator", + 0x13: "Format error during RF communication", + 0x14: "Mifare authentication error", + 0x18: "Target or Initiator does not support NFC Secure", + 0x19: "I2C bus line is busy, a TDA transaction is ongoing", + 0x23: "ISO/IEC14443-3 UID check byte is wrong", + 0x25: "Command invalid in current DEP state", + 0x26: "Operation not allowed in this configuration", + 0x27: "Command is not acceptable due to the current context", + 0x29: "Released by Initiator while operating as Target", + 0x2A: "ISO/IEC14443-3B, the ID of the card does not match", + 0x2B: "ISO/IEC14443-3B, card previously activated has disappeared", + 0x2C: "NFCID3i and NFCID3t mismatch in DEP 212/424 kbps passive", + 0x2D: "An over-current event has been detected", + 0x2E: "NAD missing in DEP frame", + 0x7f: "Invalid command syntax - received error frame", + 0xff: "Insufficient data received from executing chip command", + } + + host_command_frame_max_size = 265 + in_list_passive_target_max_target = 1 + in_list_passive_target_brty_range = (0, 1, 2, 3, 4, 6, 7, 8) + + def get_general_status(self): + data = super(Chipset, self).get_general_status() + err = self.ERR.get(data[0], "error code 0x%02X" % data[0]) + field = ("", "external field detected")[data[1]] + if data[2] == 1: + br_rx = (106, 212, 424, 848)[data[4]] + br_tx = (106, 212, 424, 848)[data[5]] + mtype = {0: "A/B", 1: "Active", 2: "Jewel", 16: "FeliCa"}[data[6]] + return err, field, (data[3], br_rx, br_tx, mtype) + else: + return err, field, None + + def _read_register(self, data): + data = self.command(0x06, data, timeout=0.25) + if data[0] != 0: + self.chipset_error(data) + return data[1:] + + def _write_register(self, data): + data = self.command(0x08, data, timeout=0.25) + if data[0] != 0: + self.chipset_error(data) + + def tg_init_as_target(self, mode, mifare_params, felica_params, + nfcid3t, gt, tk, timeout): + assert type(mode) is int and mode & 0b11111100 == 0 + assert len(mifare_params) == 6 + assert len(felica_params) == 18 + assert len(nfcid3t) == 10 + + data = (bytearray([mode]) + mifare_params + felica_params + nfcid3t + + bytearray([len(gt)]) + gt + bytearray([len(tk)]) + tk) + return self.command(0x8c, data, timeout) + + +class Device(pn53x.Device): + # Device driver for PN533 based contactless frontends. + + def __init__(self, chipset, logger): + assert isinstance(chipset, Chipset) + super(Device, self).__init__(chipset, logger) + + ic, ver, rev, support = self.chipset.get_firmware_version() + self._chipset_name = "PN5{0:02x}v{1}.{2}".format(ic, ver, rev) + self.log.debug("chipset is a {0}".format(self._chipset_name)) + + self.mute() + self.chipset.rf_configuration(0x02, b"\x00\x0B\x0A") + self.chipset.rf_configuration(0x04, b"\x00") + self.chipset.rf_configuration(0x05, b"\x01\x00\x01") + self.chipset.set_parameters(0b00000000) + + self.eeprom = bytearray() + try: + self.chipset.read_register(0xA000) # check access + for addr in range(0xA000, 0xA100, 64): + data = self.chipset.read_register(*range(addr, addr+64)) + self.eeprom.extend(data) + except Chipset.Error: + self.log.debug("no eeprom attached") + + if self.eeprom: + head = "EEPROM " + ' '.join(["%2X" % i for i in range(16)]) + self.log.debug(head) + for i in range(0, len(self.eeprom), 16): + data = ' '.join(["%02X" % x for x in self.eeprom[i:i+16]]) + self.log.debug(('0x%04X: %s' % (0xA000+i, data))) + else: + self.log.debug("no eeprom attached") + + self.log.debug("write analog settings for Type A 106 kbps") + data = bytearray.fromhex("5A F4 3F 11 4D 85 61 6F 26 62 87") + self.chipset.rf_configuration(0x0A, data) + + self.log.debug("write analog settings for Type F 212/424 kbps") + data = bytearray.fromhex("6A FF 3F 10 41 85 61 6F") + self.chipset.rf_configuration(0x0B, data) + + self.log.debug("write analog settings for Type B 106 kbps") + data = bytearray.fromhex("FF 04 85") + self.chipset.rf_configuration(0x0C, data) + + self.log.debug("write analog settings for 14443-4 212/424/848 kbps") + data = bytearray.fromhex("85 15 8A 85 0A B2 85 04 DA") + self.chipset.rf_configuration(0x0D, data) + + def close(self): + self.mute() + super(Device, self).close() + + def sense_tta(self, target): + """Activate the RF field and probe for a Type A Target. + + The PN533 can discover all kinds of Type A Targets (Type 1 + Tag, Type 2 Tag, and Type 4A Tag) at 106 kbps. + + """ + return super(Device, self).sense_tta(target) + + def sense_ttb(self, target): + """Activate the RF field and probe for a Type B Target. + + The PN533 can discover Type B Targets (Type 4B Tag) at 106, + 212, 424, and 848 kbps. The PN533 automatically sends an + ATTRIB command that configures a 64 byte maximum frame + size. The driver reverts this configuration with a DESELECT + and WUPB command to return the target prepared for activation. + + """ + return super(Device, self).sense_ttb(target) + + def sense_ttf(self, target): + """Activate the RF field and probe for a Type F Target. + + The PN533 can discover Type F Targets (Type 3 Tag) at 212 and + 424 kbps. + + """ + return super(Device, self).sense_ttf(target) + + def sense_dep(self, target): + """Search for a DEP Target in active communication mode.""" + return super(Device, self).sense_dep(target) + + def send_cmd_recv_rsp(self, target, data, timeout): + """Send command *data* to the remote *target* and return the response + data if received within *timeout* seconds. + + """ + return super(Device, self).send_cmd_recv_rsp(target, data, timeout) + + def _tt1_send_cmd_recv_rsp(self, data, timeout): + # Special handling for Tag Type 1 (Jewel/Topaz) card commands. + + if data[0] in (0x00, 0x01, 0x1A, 0x53, 0x72): + # RALL, READ, WRITE-NE, WRITE-E, RID are properly + # implemented by the PN533 firmware. + return self.chipset.in_data_exchange(data, timeout)[0] + + if data[0] == 0x10: + # RSEG implementation does not accept any segment other + # than 0. Unfortunately we can not directly issue this + # command to the CIU because the response is 128 byte and + # we're not fast enough to read it from the 64 byte FIFO. + rsp = data[1:2] + for block in range((data[1] >> 4) * 16, (data[1] >> 4) * 16 + 16): + cmd = bytearray([0x02, block]) + data[2:] + rsp += self._tt1_send_cmd_recv_rsp(cmd, timeout)[1:9] + return rsp + + # Remaining commands READ8, WRITE-E8, WRITE-NE8 are not + # implemented by the chipset. Fortunately we can directly + # program the CIU through register read/write. Each TT1 + # command byte must be send as a separate Type A frame, the + # first is a short frame with only 7 data bits and the others + # are normal frames. Reading is also a bit complicated because + # for sending we have to disable the parity generator which + # means that we will also receive the parity bits, thus 9 bits + # received per 8 data bits. And because they are already + # reversed in the FIFO we must swap before parity removal and + # afterwards (maybe this could be a bit more optimized). + data = self.add_crc_b(data) + self.chipset.write_register( + ("CIU_FIFOData", data[0]), # CMD_CODE + ("CIU_ManualRCV", 0x10), # ParityDisable + ("CIU_BitFraming", 0x07), # 7 bits + ("CIU_Command", 0x04), # Transmit + ) + for i in range(1, len(data)-1): + self.chipset.write_register( + ("CIU_FIFOData", data[i]), # CMD_DATA + ("CIU_BitFraming", 0x00), # 8 bits + ("CIU_Command", 0x04), # Transmit + ) + self.chipset.write_register( + ("CIU_FIFOData", data[-1]), # CMD_DATA + ("CIU_Command", 0x0C), # Transceive + ("CIU_BitFraming", 0x80), # 8 bits, start send + ) + if data[0] == 0x54: # WRITE-E8 + time.sleep(0.006) # assuming same response time as WRITE-E + if data[0] == 0x1B: # WRITE-NE8 + time.sleep(0.003) # assuming same response time as WRITE-NE + self.chipset.write_register(("CIU_ManualRCV", 0x00)) # enable parity + fifo_level = self.chipset.read_register("CIU_FIFOLevel") + if fifo_level == 0: + raise nfc.clf.TimeoutError + data = self.chipset.read_register(*(fifo_level * ["CIU_FIFOData"])) + data = ''.join(["{:08b}".format(octet)[::-1] for octet in data]) + data = [int(data[i:i+8][::-1], 2) for i in range(0, len(data)-8, 9)] + if self.check_crc_b(data) is False: + raise nfc.clf.TransmissionError("crc_b check error") + return bytearray(data[:-2]) + + def listen_tta(self, target, timeout): + """Listen *timeout* seconds for a Type A activation at 106 kbps. The + ``sens_res``, ``sdd_res``, and ``sel_res`` response data must + be provided and ``sdd_res`` must be a 4 byte UID that starts + with ``08h``. Depending on ``sel_res`` an activation may + return a target with a ``tt2_cmd``, ``tt4_cmd`` or ``atr_req`` + attribute. The default RATS response sent for a Type 4 Tag + activation can be replaced with a ``rats_res`` attribute. + + """ + return super(Device, self).listen_tta(target, timeout) + + def listen_ttb(self, target, timeout): + """Listen as Type B Target is not supported.""" + info = "{device} does not support listen as Type B Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_ttf(self, target, timeout): + """Listen *timeout* seconds for a Type F card activation. The target + ``brty`` must be set to either 212F or 424F and ``sensf_res`` + provide 19 byte response data (response code + 8 byte IDm + 8 + byte PMm + 2 byte system code). Note that the maximum command + an response frame length is 64 bytes only (including the frame + length byte), because the driver must directly program the + contactless interface unit within the PN533. + + """ + return super(Device, self).listen_ttf(target, timeout) + + def listen_dep(self, target, timeout): + """Listen *timeout* seconds to become initialized as a DEP Target. + + The PN533 can be set to listen as a DEP Target for passive and + active communication mode. + + """ + return super(Device, self).listen_dep(target, timeout) + + def send_rsp_recv_cmd(self, target, data, timeout): + """While operating as *target* send response *data* to the remote + device and return new command data if received within + *timeout* seconds. + + """ + return super(Device, self).send_rsp_recv_cmd(target, data, timeout) + + def _init_as_target(self, mode, tta_params, ttf_params, timeout): + nfcid3t = ttf_params[0:8] + b"\x00\x00" + args = (mode, tta_params, ttf_params, nfcid3t, b'', b'', timeout) + return self.chipset.tg_init_as_target(*args) + + +def init(transport): + # write ack to perform a soft reset, raises IOError(EACCES) if + # someone else has already claimed the USB device. + transport.write(Chipset.ACK) + + chipset = Chipset(transport, logger=log) + device = Device(chipset, logger=log) + + # PN533 bug: Manufacturer and product strings are no longer + # accessible from USB device description after first use with + # slightly larger command frames. Better read it from EEPROM. + if device.eeprom: + index = 0 + while index < len(device.eeprom) and device.eeprom[index] != 0xFF: + tlv_tag, tlv_len = device.eeprom[index], device.eeprom[index+1] + tlv_data = device.eeprom[index+2:index+2+tlv_len] + if tlv_tag == 3: + device._device_name = tlv_data[2:].decode("utf-16-le") + if tlv_tag == 4: + device._vendor_name = tlv_data[2:].decode("utf-16-le") + index += 2 + tlv_len + else: + device._vendor_name = "SensorID" + device._device_name = "StickID" + + return device diff --git a/src/lib/nfc/clf/pn53x.py b/src/lib/nfc/clf/pn53x.py new file mode 100644 index 0000000..6ef9fa0 --- /dev/null +++ b/src/lib/nfc/clf/pn53x.py @@ -0,0 +1,1064 @@ +# -*- coding: latin-1 -*- + +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""This is not really a device driver but a base module that +implements common functionality for the PN53x family of contactless +interface chips, namely the NXP PN531, PN532, PN533 and the Sony +RC-S956. + +""" +import nfc.clf +from . import device + +import os +import time +import errno +from binascii import hexlify +from struct import pack, unpack + +import logging +log = logging.getLogger(__name__) + + +class Chipset(object): + SOF = bytearray.fromhex('0000FF') + ACK = bytearray.fromhex('0000FF00FF00') + REG = { + 0x6331: "CIU_Command", + 0x6332: "CIU_CommIEn", + 0x6333: "CIU_DivIEn", + 0x6334: "CIU_CommIRq", + 0x6335: "CIU_DivIRq", + 0x6336: "CIU_Error", + 0x6337: "CIU_Status1", + 0x6338: "CIU_Status2", + 0x6339: "CIU_FIFOData", + 0x633A: "CIU_FIFOLevel", + 0x633B: "CIU_WaterLevel", + 0x633C: "CIU_Control", + 0x633D: "CIU_BitFraming", + 0x633E: "CIU_Coll", + 0x6301: "CIU_Mode", + 0x6302: "CIU_TxMode", + 0x6303: "CIU_RxMode", + 0x6304: "CIU_TxControl", + 0x6305: "CIU_TxAuto", + 0x6306: "CIU_TxSel", + 0x6307: "CIU_RxSel", + 0x6308: "CIU_RxThreshold", + 0x6309: "CIU_Demod", + 0x630A: "CIU_FelNFC1", + 0x630B: "CIU_FelNFC2", + 0x630C: "CIU_MifNFC", + 0x630D: "CIU_ManualRCV", + 0x630E: "CIU_TypeB", + 0x630F: "CIU_SerialSpeed", + 0x6311: "CIU_CRCResultMSB", + 0x6312: "CIU_CRCResultLSB", + 0x6313: "CIU_GsNOff", + 0x6314: "CIU_ModWidth", + 0x6315: "CIU_TxBitPhase", + 0x6316: "CIU_RFCfg", + 0x6317: "CIU_GsNOn", + 0x6318: "CIU_CWGsP", + 0x6319: "CIU_ModGsP", + 0x631A: "CIU_TMode", + 0x631B: "CIU_TPrescaler", + 0x631C: "CIU_TReloadHi", + 0x631D: "CIU_TReloadLo", + 0x631E: "CIU_TCounterHi", + 0x631F: "CIU_TCounterLo", + 0x6321: "CIU_TestSel1", + 0x6322: "CIU_TestSel2", + 0x6323: "CIU_TestPinEn", + 0x6324: "CIU_TestPinValue", + 0x6325: "CIU_TestBus", + 0x6326: "CIU_AutoTest", + 0x6327: "CIU_Version", + 0x6328: "CIU_AnalogTest", + 0x6329: "CIU_TestDAC1", + 0x632A: "CIU_TestDAC2", + 0x632B: "CIU_TestADC", + 0x632C: "CIU_RFT1", + 0x632D: "CIU_RFT2", + 0x632E: "CIU_RFT3", + 0x632F: "CIU_RFT4", + } + REGBYNAME = {v: k for k, v in REG.items()} + + class Error(Exception): + def __init__(self, errno, strerr): + self.errno, self.strerr = errno, strerr + + def __str__(self): + return "Error 0x{0:02X}: {1}".format(self.errno, self.strerr) + + def chipset_error(self, cause): + if cause is None: + errno = 0xff + elif type(cause) is int: + errno = cause + else: + errno = cause[0] + + strerr = self.ERR.get(errno, "Unknown error code") + raise Chipset.Error(errno, strerr) + + def __init__(self, transport, logger): + self.transport = transport + self.log = logger + + def close(self): + self.transport.close() + self.transport = None + + def command(self, cmd_code, cmd_data, timeout): + """Send a host command and return the chip response. The chip command + is selected by the 8-bit integer *cmd_code*. The command + parameters, if any, are supplied with *cmd_data* as a + bytearray or byte string. The fully constructed command frame + is sent with :meth:`write_frame` and the chip acknowledgement + and response is received with :meth:`read_frame`, those + methods are used by some drivers for additional framing. The + implementation waits 100 ms for the command acknowledgement + and then polls every 100 ms for a response frame until + *timeout* seconds have elapsed. If the response frame is + correct and the response code matches *cmd_code* the data + bytes that follow the response code are returned as a + bytearray (without the trailing checksum and postamble). + + **Exceptions** + + * :exc:`~exceptions.IOError` :const:`errno.ETIMEDOUT` if no + response frame was received before *timeout* seconds. + + * :exc:`~exceptions.IOError` :const:`errno.EIO` if response + frame errors were detected. + + * :exc:`Chipset.Error` if an error response frame or status + error was received. + + """ + if cmd_data is not None: + assert len(cmd_data) <= self.host_command_frame_max_size - 2 + self.log.log(logging.DEBUG-1, "{} {} {:.3f}".format( + self.CMD[cmd_code], hexlify(cmd_data).decode(), timeout)) + + if len(cmd_data) < 254: + head = self.SOF + bytearray([len(cmd_data)+2]) \ + + bytearray([254-len(cmd_data)]) + else: + head = self.SOF + b'\xFF\xFF' + pack(">H", len(cmd_data)+2) + head.append((256 - sum(head[-2:])) & 0xFF) + + data = bytearray([0xD4, cmd_code]) + cmd_data + tail = bytearray([(256 - sum(data)) & 0xFF, 0]) + + try: + self.write_frame(head + data + tail) + frame = self.read_frame(timeout=100) + except IOError: + self.log.error("input/output error while waiting for ack") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + if not frame.startswith(self.SOF): + self.log.error("invalid frame start sequence") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + if frame[0:len(self.ACK)] != self.ACK: + self.log.warning("missing ack frame") + else: + frame = self.ACK + + if timeout is not None and timeout <= 0: + return + + while frame == self.ACK: + try: + frame = self.read_frame(int(1000 * timeout)) + except IOError as error: + if error.errno == errno.ETIMEDOUT: + self.write_frame(self.ACK) # cancel command + time.sleep(0.001) + raise error + + if frame.startswith(self.SOF + b'\xFF\xFF'): + # extended frame + if sum(frame[5:8]) & 0xFF != 0: + self.log.error("frame lenght checksum error") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + if unpack(">H", memoryview(frame[5:7]))[0] != len(frame) - 10: + self.log.error("frame lenght value mismatch") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + del frame[0:8] + elif frame.startswith(self.SOF): + # normal frame + if sum(frame[3:5]) & 0xFF != 0: + self.log.error("frame lenght checksum error") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + if frame[3] != len(frame) - 7: + self.log.error("frame lenght value mismatch") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + del frame[0:5] + else: + self.log.debug("invalid frame start sequence") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + if not sum(frame) & 0xFF == 0: + self.log.error("frame data checksum error") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + if frame[0] == 0x7F: # error frame + self.chipset_error(0x7F) + + if not frame[0] == 0xD5: + self.log.error("invalid frame identifier") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + if not frame[1] == cmd_code + 1: + self.log.error("unexpected response code") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + return frame[2:-2] + + def write_frame(self, frame): + """Write a command *frame* to the chipset.""" + self.transport.write(frame) + + def read_frame(self, timeout): + """Wait *timeout* milliseconds to return a chip response frame.""" + return self.transport.read(timeout) + + def send_ack(self): + # Send an ACK frame, usually to terminate most recent command. + self.transport.write(Chipset.ACK) + + def diagnose(self, test, test_data=None): + """Send a Diagnose command. The *test* argument selects the diagnose + function either by number or the string ``line``, ``rom``, or + ``ram``. For a ``line`` test the implementation sends the + longest possible command frame and verifies that the response + data is identical. For a ``ram`` or ``rom`` test the + implementation verfies the response status. For a *test* + number the implementation appends the byte string *test_data* + and returns the response data bytes. + + """ + if test == "line": + size = self.host_command_frame_max_size - 3 + data = b'\x00' + bytearray([x & 0xFF for x in range(size)]) + return self.command(0x00, data, timeout=1.0) == data + if test == "rom": + data = self.command(0x00, b'\x01', timeout=1.0) + return data and data[0] == 0 + if test == "ram": + data = self.command(0x00, b'\x02', timeout=1.0) + return data and data[0] == 0 + return self.command(0x00, pack('B', test) + test_data, timeout=1.0) + + def get_firmware_version(self): + """Send a GetFirmwareVersion command and return the response data + bytes. + + """ + return self.command(0x02, b'', timeout=0.1) + + def get_general_status(self): + """Send a GetGeneralStatus command and return the response data + bytes. + + """ + data = self.command(0x04, b'', timeout=0.1) + if data is None or len(data) < 3: + raise self.chipset_error(None) + return data + + def read_register(self, *args): + """Send a ReadRegister command for the positional register address or + name arguments. The register values are returned as a list for + multiple arguments or an integer for a single argument. :: + + tx_mode = Chipset.read_register(0x6302) + rx_mode = Chipset.read_register("CIU_RxMode") + tx_mode, rx_mode = Chipset.read_register("CIU_TxMode", "CIU_RxMode") + + """ + def addr(r): + return self.REGBYNAME[r] if type(r) is str else r + + args = [addr(reg) for reg in args] + data = b''.join([pack(">H", reg) for reg in args]) + data = self._read_register(data) + return list(data) if len(data) > 1 else data[0] + + def _read_register(self, data): + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError(cname + "._read_register") + + def write_register(self, *args): + """Send a WriteRegister command. Each positional argument must be an + (address, value) tuple except if exactly two arguments are + supplied as register address and value. A register can also be + selected by name. There is no return value. :: + + Chipset.write_register(0x6301, 0x00) + Chipset.write_register("CIU_Mode", 0x00) + Chipset.write_register((0x6301, 0x00), ("CIU_TxMode", 0x00)) + + """ + def addr(r): + return self.REGBYNAME[r] if type(r) is str else r + + assert type(args) in (tuple, list) + if len(args) == 2 and type(args[1]) == int: + args = [args] + args = [(addr(reg), val) for reg, val in args] + data = b''.join([pack(">HB", reg, val) for reg, val in args]) + self._write_register(data) + + def _write_register(self, data): + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError(cname + "._write_register") + + def set_parameters(self, flags): + """Send a SetParameters command with the 8-bit *flags* integer.""" + self.command(0x12, bytearray([flags]), timeout=0.1) + + def rf_configuration(self, cfg_item, cfg_data): + """Send an RFConfiguration command.""" + self.command(0x32, bytearray([cfg_item]) + bytearray(cfg_data), + timeout=0.1) + + def in_jump_for_dep(self, act_pass, br, passive_data, nfcid3, gi): + """Send an InJumpForDEP command. + + """ + assert act_pass in (False, True) + assert br in (106, 212, 424) + assert len(passive_data) in (0, 4, 5) + assert len(nfcid3) in (0, 10) + assert len(gi) <= 48 + cm = int(bool(act_pass)) + br = (106, 212, 424).index(br) + nf = (bool(passive_data) | bool(nfcid3) << 1 | bool(gi) << 2) + data = bytearray([cm, br, nf]) + passive_data + nfcid3 + gi + data = self.command(0x56, bytearray(data), timeout=3.0) + if data is None or data[0] != 0: + self.chipset_error(data) + return data[2:] + + def in_jump_for_psl(self, act_pass, br, passive_data, nfcid3, gi): + """Send an InJumpForPSL command. + + """ + assert act_pass in (False, True) + assert br in (106, 212, 424) + assert len(passive_data) in (0, 4, 5) + assert len(nfcid3) in (0, 10) + assert len(gi) <= 48 + cm = int(bool(act_pass)) + br = (106, 212, 424).index(br) + nf = (bool(passive_data) | bool(nfcid3) << 1 | bool(gi) << 2) + data = bytearray([cm, br, nf]) + passive_data + nfcid3 + gi + data = self.command(0x46, data, timeout=3.0) + if data is None or data[0] != 0: + self.chipset_error(data) + return data[2:] + + def in_list_passive_target(self, max_tg, brty, initiator_data): + assert max_tg <= self.in_list_passive_target_max_target + assert brty in self.in_list_passive_target_brty_range + data = bytearray([1, brty]) + initiator_data + data = self.command(0x4A, data, timeout=1.0) + return data[2:] if data and data[0] > 0 else None + + def in_atr(self, nfcid3i=b'', gi=b''): + flag = int(bool(nfcid3i)) | (int(bool(gi)) << 1) + data = bytearray([1, flag]) + nfcid3i + gi + data = self.command(0x50, data, timeout=1.5) + if data is None or data[0] != 0: + self.chipset_error(data) + return data[1:] + + def in_psl(self, br_it, br_ti): + data = bytearray([1, br_it, br_ti]) + data = self.command(0x4E, data, timeout=1.0) + if data is None or data[0] != 0: + self.chipset_error(data) + + def in_data_exchange(self, data, timeout, more=False): + data = self.command(0x40, bytearray([int(more) << 6 | 0x01]) + data, + timeout) + if data is None or data[0] & 0x3f != 0: + self.chipset_error(data[0] & 0x3f if data else None) + return data[1:], bool(data[0] & 0x40) + + def in_communicate_thru(self, data, timeout): + data = self.command(0x42, data, timeout) + if timeout > 0: + if data and data[0] == 0: + return data[1:] + else: + self.chipset_error(data) + + def tg_set_general_bytes(self, gb): + data = self.command(0x92, gb, timeout=0.1) + if data is None or data[0] != 0: + self.chipset_error(data) + + def tg_get_data(self, timeout): + data = self.command(0x86, b'', timeout) + if data is None or data[0] & 0x3f != 0: + self.chipset_error(data[0] & 0x3f if data else None) + return data[1:], bool(data[0] & 0x40) + + def tg_set_data(self, data, timeout): + data = self.command(0x8E, data, timeout) + if data is None or data[0] != 0: + self.chipset_error(data) + + def tg_set_meta_data(self, data, timeout): + data = self.command(0x94, data, timeout) + if data is None or data[0] != 0: + self.chipset_error(data) + + def tg_get_initiator_command(self, timeout): + data = self.command(0x88, b'', timeout) + if timeout > 0: + if data and data[0] == 0: + return data[1:] + else: + self.chipset_error(data) + + def tg_response_to_initiator(self, data): + data = self.command(0x90, data, timeout=1.0) + if data is None or data[0] != 0: + self.chipset_error(data) + + def tg_get_target_status(self): + data = self.command(0x8A, b'', timeout=0.1) + if data[0] == 0x01: + br_tx = (106, 212, 424)[data[1] >> 4 & 7] + br_rx = (106, 212, 424)[data[1] & 7] + else: + br_tx, br_rx = (0, 0) + return data[0], br_tx, br_rx + + +class Device(device.Device): + # Base class for devices with an NXP PN531, PN532, PN533 or Sony + # RC-S956 contactless interface chip. This class implements the + # functionality that is identical or needed by most of the drivers + # that inherit from pn53x. + + def __init__(self, chipset, logger): + self.chipset = chipset + self.log = logger + + try: + chipset_communication = self.chipset.diagnose('line') + except Chipset.Error: + chipset_communication = False + + if chipset_communication is False: + self.log.error("chipset communication test failed") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + # for line in self._print_ciu_register_page(0, 1, 2, 3): + # self.log.debug(line) + + # for addr in range(0, 0x03FF, 16): + # xram = self.chipset.read_register(*range(addr, addr+16)) + # xram = ' '.join(["%02X" % x for x in xram]) + # self.log.debug("0x%04X: %s", addr, xram) + + def close(self): + self.chipset.close() + self.chipset = None + + def mute(self): + self.chipset.rf_configuration(0x01, bytearray([0b00000010])) + + def sense_tta(self, target): + brty = {"106A": 0}.get(target.brty) + if brty not in self.chipset.in_list_passive_target_brty_range: + message = "unsupported bitrate {0}".format(target.brty) + self.log.warning(message) + raise ValueError(message) + + uid = target.sel_req if target.sel_req else bytearray() + if len(uid) > 4: + uid = b'\x88' + uid + if len(uid) > 8: + uid = uid[0:4] + b'\x88' + uid[4:] + + rsp = self.chipset.in_list_passive_target(1, 0, uid) + if rsp is not None: + sens_res, sel_res, sdd_res = rsp[1::-1], rsp[2:3], rsp[4:] + if sel_res[0] & 0x60 == 0x00: + self.log.debug("disable crc check for type 2 tag") + rxmode = self.chipset.read_register("CIU_RxMode") + self.chipset.write_register("CIU_RxMode", rxmode & 0x7F) + return nfc.clf.RemoteTarget( + "106A", sens_res=sens_res, sel_res=sel_res, sdd_res=sdd_res) + + if self.chipset.read_register("CIU_FIFOData") == 0x26: + # If we still see the SENS_REQ command in the CIU FIFO + # then there was no SENS_RES, thus no tag present. + return None + + self.log.debug("sens_res but no sdd_res, try as type 1 tag") + + if 4 not in self.chipset.in_list_passive_target_brty_range: + self.log.warning("The {0} can not read Type 1 Tags.".format(self)) + return None + + rsp = self.chipset.in_list_passive_target(1, 4, b"") + if rsp is not None: + rid_cmd = bytearray.fromhex("78 0000 00000000") + try: + rid_res = self.chipset.in_data_exchange(rid_cmd, 0.01)[0] + return nfc.clf.RemoteTarget( + "106A", sens_res=rsp[1::-1], rid_res=rid_res) + except Chipset.Error: + pass + + def sense_ttb(self, target, did=None): + brty = {"106B": 3, "212B": 6, "424B": 7, "848B": 8}.get(target.brty) + if brty not in self.chipset.in_list_passive_target_brty_range: + message = "unsupported bitrate {0}".format(target.brty) + self.log.warning(message) + raise ValueError(message) + + afi = target.sensb_req[0:1] if target.sensb_req else b'\x00' + rsp = self.chipset.in_list_passive_target(1, brty, afi) + if rsp and rsp[10] & 0b00001001 == 0b00000001: + # This is an ISO tag and the chipset has now activated it + # with 64-byte max frame size and maybe a DID. Because we + # implement ISO-DEP in software and can do without DID and + # use a full 256 byte response frame size, we'll send a + # DESELECT and WUPB to allow ATTRIB from the activation + # code in tags/tt4.py. + try: + deselect_command = (b'\xCA' + did) if did else b'\xC2' + wupb_command = b'\x05' + afi + b'\x08' + self.chipset.in_communicate_thru(deselect_command, 0.5) + rsp = self.chipset.in_communicate_thru(wupb_command, 0.5) + return nfc.clf.RemoteTarget(target.brty, sensb_res=rsp) + except (Chipset.Error, IOError) as error: + self.log.debug(error) + + def sense_ttf(self, target): + brty = {"212F": 1, "424F": 2}.get(target.brty) + if brty not in self.chipset.in_list_passive_target_brty_range: + message = "unsupported bitrate {0}".format(target.brty) + self.log.warning(message) + raise ValueError(message) + + if not self.chipset.read_register("CIU_TxControl") & 0b00000011: + # Some FeliCa cards need more time from power up to + # polling. If the field was not already activated, do this + # now and wait about 5 ms. + self.chipset.rf_configuration(0x01, b'\x01') + time.sleep(0.005) + + default_sensf_req = bytearray.fromhex("00FFFF0100") + sensf_req = target.sensf_req if target.sensf_req else default_sensf_req + rsp = self.chipset.in_list_passive_target(1, brty, sensf_req) + if rsp is not None: + return nfc.clf.RemoteTarget(target.brty, sensf_res=rsp[1:]) + + def sense_dep(self, target): + # Attempt active communication mode target activation. + assert target.atr_req, "the target.atr_req attribute is required" + assert len(target.atr_req) >= 16, "minimum lenght of atr_req is 16" + assert len(target.atr_req) <= 64, "maximum lenght of atr_req is 64" + + # bitrate and modulation type for send/recv must be set and equal + assert target.brty_send and target.brty_recv + assert target.brty_send == target.brty_recv + + br = int(target.brty[0:-1]) + nfcid3 = target.atr_req[2:12] + gbytes = target.atr_req[16:] + try: + data = self.chipset.in_jump_for_psl(1, br, b'', nfcid3, gbytes) + atr_res = b'\xD5\x01' + data + except Chipset.Error as error: + if error.errno not in (0x01, 0x0A): + self.log.error(error) + return None + finally: + # unset the detect-sync bit, 106A sync byte is handled in dep.py + self.chipset.write_register("CIU_Mode", 0b00111011) + + self.log.debug("running DEP in {0} kbps active mode".format(br)) + return nfc.clf.RemoteTarget(target.brty, atr_res=atr_res, + atr_req=target.atr_req) + + def get_max_send_data_size(self, target): + return self.chipset.host_command_frame_max_size - 2 + + def get_max_recv_data_size(self, target): + return self.chipset.host_command_frame_max_size - 3 + + def send_cmd_recv_rsp(self, target, data, timeout): + def bitrate(brty): + return [106 << i for i in range(6)].index(int(brty[:-1])) + + def framing(brty): + return {'A': 0b00, 'B': 0b11, 'F': 0b10}[brty[-1:]] + + # Set bitrate and modulation type for send and receive. + acm = target.atr_res and not (target.sens_res or target.sensf_res) + reg = ("CIU_TxMode", "CIU_RxMode", "CIU_TxAuto") + txm, rxm, txa = self.chipset.read_register(*reg) + txm = (txm & 0b10001111) | (bitrate(target.brty_send) << 4) + rxm = (rxm & 0b10001111) | (bitrate(target.brty_recv) << 4) + txm = (txm & 0b11111100) | (0b01 if acm else framing(target.brty_send)) + rxm = (rxm & 0b11111100) | (0b01 if acm else framing(target.brty_recv)) + txa = (txa & 0b10111111) | (target.brty_send.endswith("A") << 6) + reg = (("CIU_TxMode", txm), ("CIU_RxMode", rxm), ("CIU_TxAuto", txa)) + self.chipset.write_register(*reg) + + # Calculate the timeout index for InCommunicateThru. The + # effective timeout is T(us) = 100 * 2**(n-1) for 1 <= n <= 16 + # and "no timeout" for n = 0. For a given timeout we calculate + # the index as the first effective timeout that is longer. + timeout_microsec = int(timeout * 1E6) + try: + index = [i+1 for i in range(16) if timeout_microsec >> i <= 100][0] + except IndexError: + index = 16 + timeout_microsec = 100 << (index-1) + timeout = (100 << (index-1)) / 1E6 + self.log.log(logging.DEBUG-1, "set response timeout %.6f sec", timeout) + self.chipset.rf_configuration(0x02, bytearray([10, 11, index])) + + # Send the command data and return the response. All cases + # where a response is not received raise either an IOError + # or one of the nfc.clf.CommunicationError specializations. + data = bytearray(data) if not isinstance(data, bytearray) else data + try: + if target.sens_res and not target.atr_res: + if target.rid_res: # TT1 + return self._tt1_send_cmd_recv_rsp(data, timeout+0.1) + if target.sel_res[0] & 0x60 == 0x00: # TT2 + return self._tt2_send_cmd_recv_rsp(data, timeout+0.1) + return self.chipset.in_communicate_thru(data, timeout+0.1) + except Chipset.Error as error: + self.log.debug(error) + if error.errno == 1: + raise nfc.clf.TimeoutError + else: + raise nfc.clf.TransmissionError(str(error)) + except IOError as error: + self.log.debug(error) + if not error.errno == errno.ETIMEDOUT: + raise error + else: + raise nfc.clf.TimeoutError("send_cmd_recv_rsp") + + def _tt1_send_cmd_recv_rsp(self, data, timeout): + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError(cname + "._tt1_send_cmd_recv_rsp()") + + def _tt2_send_cmd_recv_rsp(self, data, timeout): + # The Type2Tag implementation needs to receive the Mifare + # ACK/NAK responses but the chipset reports them as crc error + # (indistinguishable from a real crc error). We thus have to + # switch off the crc check and do it here. + data = self.chipset.in_communicate_thru(data, timeout) + if len(data) > 2 and self.check_crc_a(data) is False: + raise nfc.clf.TransmissionError("crc_a check error") + return data[:-2] if len(data) > 2 else data + + def listen_tta(self, target, timeout): + if target.brty != "106A": + info = "unsupported bitrate/type: %r" % target.brty + raise nfc.clf.UnsupportedTargetError(info) + if target.rid_res: + info = "listening for type 1 tag activation is not supported" + raise nfc.clf.UnsupportedTargetError(info) + try: + assert target.sens_res is not None, "sens_res is required" + assert target.sdd_res is not None, "sdd_res is required" + assert target.sel_res is not None, "sel_res is required" + assert len(target.sens_res) == 2, "sens_res must be 2 byte" + assert len(target.sdd_res) == 4, "sdd_res must be 4 byte" + assert len(target.sel_res) == 1, "sel_res must be 1 byte" + assert target.sdd_res[0] == 0x08, "sdd_res[0] must be 08h" + except AssertionError as error: + raise ValueError(str(error)) + + nfcf_params = bytearray(range(18)) + nfca_params = target.sens_res + target.sdd_res[1:4] + target.sel_res + self.log.debug("nfca_params %s", hexlify(nfca_params).decode()) + + # We can use TgInitAsTarget to exclusively answer Type A + # activation when the CIU automatic mode detector is disabled + # (the firmware does not unset or even check this bit). When + # TgInitAsTarget prepares for AutoColl, the firmware also sets + # the CIU_TxMode and CIU_RXMode to 106A. + self.chipset.write_register("CIU_Mode", 0b00111111) + + time_to_return = time.time() + timeout + while time.time() < time_to_return: + try: + wait = max(time_to_return - time.time(), 0.5) + args = (1, nfca_params, nfcf_params, wait) + data = self._init_as_target(*args) + except IOError as error: + if error.errno != errno.ETIMEDOUT: + raise error + else: + return None + + brty = ("106A", "212F", "424F")[(data[0] & 0x70) >> 4] + self.log.debug("%s rcvd %s", + brty, hexlify(memoryview(data)[1:]).decode()) + if brty != target.brty or len(data) < 2: + log.debug("received bitrate does not match %s", target.brty) + continue + + if target.sel_res[0] & 0x60 == 0x00: + self.log.debug("rcvd TT2_CMD %s", + hexlify(memoryview(data)[1:]).decode()) + target = nfc.clf.LocalTarget(brty, tt2_cmd=data[1:]) + target.sens_res = nfca_params[0:2] + target.sdd_res = b'\x08' + nfca_params[2:5] + target.sel_res = nfca_params[5:6] + return target + + elif target.sel_res[0] & 0x20 == 0x20 and data[1] == 0xE0: + default_rats_res = bytearray.fromhex("05 78 80 70 02") + (rats_cmd, rats_res) = (data[1:], target.rats_res) + if not rats_res: + rats_res = default_rats_res + self.log.debug("rcvd RATS_CMD %s", hexlify(rats_cmd).decode()) + self.log.debug("send RATS_RES %s", hexlify(rats_res).decode()) + try: + self.chipset.tg_response_to_initiator(rats_res) + data = self.chipset.tg_get_initiator_command(1.0) + except (Chipset.Error, IOError) as error: + self.log.error(error) + return + if data and data[0] & 0xF0 == 0xC0: # S(DESELECT) + self.log.debug("rcvd S(DESELECT) %s", + hexlify(data).decode()) + self.log.debug("send S(DESELECT) %s", + hexlify(data).decode()) + self.chipset.tg_response_to_initiator(data) + elif data: + self.log.debug("rcvd TT4_CMD %s", + hexlify(data).decode()) + target = nfc.clf.LocalTarget(brty, tt4_cmd=data) + target.sens_res = nfca_params[0:2] + target.sdd_res = b'\x08' + nfca_params[2:5] + target.sel_res = nfca_params[5:6] + return target + + elif (target.sel_res[0] & 0x40 and data[1] == 0xF0 + and len(data) >= 19 and data[2] == len(data)-2 + and data[3:5] == b'\xD4\x00'): + self.log.debug("rcvd ATR_REQ %s", + hexlify(memoryview(data)[3:]).decode()) + target = nfc.clf.LocalTarget(brty, atr_req=data[3:]) + target.sens_res = nfca_params[0:2] + target.sdd_res = b'\x08' + nfca_params[2:5] + target.sel_res = nfca_params[5:6] + return target + + def listen_ttf(self, target, timeout): + # For NFC-F listen we can not use TgInitAsTarget because it + # always sets CIU_TxMode and CIU_RxMode to 106A. Best we can + # do is to program the CIU AutoColl command and then work with + # the CIU to receive tag commands in _tt3_send_rsp_recv_cmd + # (InCommunicateThru does not work probably because the + # firmware is not in target state). With the 64-bit only CIU + # FIFO it means that a tag can only allow two blocks for read + # and write. + if target.brty not in ("212F", "424F"): + info = "unsupported bitrate/type: %r" % target.brty + raise nfc.clf.UnsupportedTargetError(info) + try: + assert target.sensf_res is not None, "sensf_res is required" + assert len(target.sensf_res) == 19, "sensf_res must be 19 byte" + except AssertionError as error: + raise ValueError(str(error)) + + nfca_params = bytearray(6) + nfcf_params = bytearray(target.sensf_res[1:]) + self.log.debug("nfcf_params %s", hexlify(nfcf_params).decode()) + + regs = [ + ("CIU_Command", 0b00000000), # Idle command + ("CIU_FIFOLevel", 0b10000000), # clear fifo + ] + regs.extend(zip(25*["CIU_FIFOData"], + nfca_params + nfcf_params + b"\0")) + regs.append(("CIU_Command", 0b00000001)) # Configure command + self.chipset.write_register(*regs) + regs = [ + ("CIU_Control", 0b00000000), # act as target (b4=0) + ("CIU_Mode", 0b00111111), # disable mode detector (b2=1) + ("CIU_FelNFC2", 0b10000000), # wait until selected (b7=1) + ("CIU_TxMode", 0b10000010 | (int(target.brty[:-1])//212) << 4), + ("CIU_RxMode", 0b10001010 | (int(target.brty[:-1])//212) << 4), + ("CIU_TxControl", 0b10000000), # disable output on TX1/TX2 + ("CIU_TxAuto", 0b00100000), # wake up when rf level detected + ("CIU_Demod", 0b01100001), # use Q channel, freeze PLL in recv + ("CIU_CommIRq", 0b01111111), # clear interrupt request bits + ("CIU_DivIRq", 0b01111111), # clear interrupt request bits + ("CIU_Command", 0b00001101), # AutoColl command + ] + self.chipset.write_register(*regs) + + regs = ("CIU_Status1", "CIU_Status2", "CIU_CommIRq", "CIU_DivIRq") + time_to_return = time.time() + timeout + while time.time() < time_to_return: + time.sleep(0.01) + status1, status2, commirq, divirq \ + = self.chipset.read_register(*regs) + if commirq & 0b00110000 == 0b00110000: + self.chipset.write_register("CIU_CommIRq", 0b00110000) + fifo_size = self.chipset.read_register("CIU_FIFOLevel") + fifo_read = fifo_size * ["CIU_FIFOData"] + fifo_data = bytearray(self.chipset.read_register(*fifo_read)) + if fifo_data and len(fifo_data) == fifo_data[0]: + self.log.debug("%s rcvd %s", target.brty, + hexlify(fifo_data).decode()) + if fifo_data[2:10] == nfcf_params[0:8]: + target = nfc.clf.LocalTarget(target.brty) + target.sensf_res = b'\x01' + nfcf_params + target.tt3_cmd = fifo_data[1:] + return target + # Restart the AutoColl command. + self.chipset.write_register("CIU_Command", 0b00001101) + self.chipset.write_register("CIU_Command", 0) # Idle command + + def listen_dep(self, target, timeout): + assert target.sensf_res is not None + assert target.sens_res is not None + assert target.sdd_res is not None + assert target.sel_res is not None + assert target.atr_res is not None + + nfca_params = target.sens_res + target.sdd_res[1:4] + target.sel_res + nfcf_params = target.sensf_res[1:19] + self.log.debug("nfca_params %s", hexlify(nfca_params).decode()) + self.log.debug("nfcf_params %s", hexlify(nfcf_params).decode()) + assert len(nfca_params) == 6 + assert len(nfcf_params) == 18 + + # enable the automatic mode detector (b2 <= 0) + self.chipset.write_register( + ("CIU_Mode", 0b01111011), # b2 - enable mode detector + ("CIU_TxMode", 0b10110000), # 848 kbps Type A framing + ("CIU_RxMode", 0b10110000)) # 848 kbps Type A framing + + time_to_return = time.time() + timeout + while time.time() < time_to_return: + try: + wait = max(time_to_return - time.time(), 0.5) + data = self._init_as_target(2, nfca_params, nfcf_params, wait) + except IOError as error: + if error.errno != errno.ETIMEDOUT: + raise error + else: + if not (data[1] == len(data)-1 and data[2:4] == b'\xD4\x00'): + self.log.debug("expected ATR_REQ but got %s", + hexlify(memoryview(data)[1:]).decode()) + else: + break + else: + return + + brty = ("106A", "212F", "424F")[(data[0] & 0b01110000) >> 4] + mode = ("passive", "active")[data[0] & 1] + self.log.debug("activated in %s %s communication mode", brty, mode) + + atr_req = data[2:] + atr_res = target.atr_res[:] + atr_res[12] = atr_req[12] # copy DID + activation_params = ((nfca_params if brty == "106A" else nfcf_params) + if mode == "passive" else None) + + try: + self.log.debug("%s send ATR_RES %s", brty, + hexlify(atr_res).decode()) + data = self._send_atr_response(atr_res, timeout=1.0) + except Chipset.Error as error: + self.log.error(error) + return + except IOError as error: + if error.errno != errno.ETIMEDOUT: + raise + self.log.debug(error) + return + + psl_req = psl_res = None + if data and data.startswith(b'\x06\xD4\x04'): + self.log.debug("%s rcvd PSL_REQ %s", brty, + hexlify(memoryview(data)[1:]).decode()) + try: + psl_req = data[1:] + assert len(psl_req) == 5, "psl_req length mismatch" + assert psl_req[2] == atr_req[12], "psl_req has wrong did" + except AssertionError as error: + log.debug(str(error)) + return None + try: + psl_res = b'\xD5\x05' + psl_req[2:3] + self.log.debug("%s send PSL_RES %s", brty, + hexlify(psl_res).decode()) + brty = self._send_psl_response(psl_req, psl_res, timeout=0.5) + data = self.chipset.tg_get_initiator_command(timeout) + except Chipset.Error as error: + self.log.error(error) + return + except IOError as error: + if error.errno != errno.ETIMEDOUT: + raise + self.log.debug(error) + return + + if data and data[0] == len(data) and data[1:3] == b'\xD4\x06': + # set detect-sync bit to 0, the 106A sync byte is handled by dep.py + self.chipset.write_register("CIU_Mode", 0b00111011) + # prepare the target description to return, exact content + # depends on how we were activated (A or F with or w/o PSL) + target = nfc.clf.LocalTarget(brty, dep_req=data[1:]) + target.atr_req, target.atr_res = atr_req, atr_res + if psl_req: + target.psl_req = psl_req + if psl_res: + target.psl_res = psl_res + if activation_params == nfca_params: + target.sens_res = nfca_params[0:2] + target.sdd_res = b'\x08' + nfca_params[2:5] + target.sel_res = nfca_params[5:6] + if activation_params == nfcf_params: + target.sensf_res = b'\x01' + nfcf_params + return target + + def _init_as_target(self, mode, tta_params, ttf_params, timeout): + cname = self.__class__.__module__ + '.' + self.__class__.__name__ + raise NotImplementedError(cname + '._init_as_target()') + + def _send_atr_response(self, atr_res, timeout): + self.chipset.tg_response_to_initiator( + bytearray([len(atr_res)+1]) + atr_res) + return self.chipset.tg_get_initiator_command(timeout) + + def _send_psl_response(self, psl_req, psl_res, timeout): + dsi = psl_req[3] >> 3 & 0b111 + dri = psl_req[3] & 0b111 + rx_mode = self.chipset.read_register("CIU_RxMode") + rx_mode = (rx_mode & 0b10001111) | (dsi << 4) + if rx_mode & 0b00000011 != 1: # if not active mode + rx_mode = (rx_mode & 0b11111100) | ((0, 2)[dsi > 0]) + self.log.debug("set CIU_RxMode to {:08b}".format(rx_mode)) + self.chipset.write_register(("CIU_RxMode", rx_mode)) + self.log.debug("send PSL_RES %s", hexlify(psl_res).decode()) + data = bytearray([len(psl_res)+1]) + psl_res + self.chipset.tg_response_to_initiator(data) + tx_mode = self.chipset.read_register("CIU_TxMode") + tx_mode = (tx_mode & 0b10001111) | (dri << 4) + if tx_mode & 0b00000011 != 1: # if not active mode + tx_mode = (tx_mode & 0b11111100) | ((0, 2)[dri > 0]) + self.log.debug("set CIU_TxMode to {:08b}".format(tx_mode)) + self.chipset.write_register(("CIU_TxMode", tx_mode)) + return ("106A", "212F", "424F")[dri] + + def _tt3_send_rsp_recv_cmd(self, target, data, timeout): + regs = [ + ("CIU_FIFOLevel", 0b10000000), # clear fifo read/write pointer + ("CIU_CommIRq", 0b01111111), # clear interrupt request bits + ("CIU_DivIRq", 0b01111111), # clear interrupt request bits + ] + if data is not None: + regs.extend(zip(len(data)*["CIU_FIFOData"], data)) + regs.append(("CIU_BitFraming", 0b10000000)) # StartSend (b7=1) + self.chipset.write_register(*regs) + + irq_regs = ("CIU_CommIRq", "CIU_DivIRq") + time_to_return = time.time() + (timeout if timeout else 0) + while timeout is None or time.time() < time_to_return: + time.sleep(0.01) + commirq, divirq = self.chipset.read_register(*irq_regs) + if divirq & 0b00000001: + raise nfc.clf.BrokenLinkError("external field switched off") + if commirq & 0b00100000: + self.chipset.write_register("CIU_CommIRq", 0b00100000) + fifo_size = self.chipset.read_register("CIU_FIFOLevel") + fifo_read = fifo_size * ["CIU_FIFOData"] + fifo_data = bytearray(self.chipset.read_register(*fifo_read)) + if fifo_data[0] != len(fifo_data): + raise nfc.clf.TransmissionError("frame length byte error") + return fifo_data + if timeout > 0: + info = "no data received within %.3f s" % timeout + self.log.debug(info) + raise nfc.clf.TimeoutError(info) + + def send_rsp_recv_cmd(self, target, data, timeout): + # print("\n".join(self._print_ciu_register_page(0, 1))) + if target.tt3_cmd: + return self._tt3_send_rsp_recv_cmd(target, data, timeout) + try: + if data: + self.chipset.tg_response_to_initiator(data) + return self.chipset.tg_get_initiator_command(timeout) + except Chipset.Error as error: + if error.errno in (0x0A, 0x29, 0x31): + self.log.debug("Error: %s", error) + raise nfc.clf.BrokenLinkError(str(error)) + else: + self.log.warning(error) + raise nfc.clf.TransmissionError(str(error)) + except IOError as error: + if error.errno == errno.ETIMEDOUT: + info = "no data received within %.3f s" % timeout + self.log.debug(info) + raise nfc.clf.TimeoutError(info) + else: + # host-controller communication broken + self.log.error(error) + raise error + + def _print_ciu_register_page(self, *pages): + lines = list() + for page in pages: + base = (0x6331, 0x6301, 0x6311, 0x6321)[page] + regs = set(self.chipset.REG) + regs = sorted(regs.intersection(range(base, base+16))) + vals = self.chipset.read_register(*regs) + regs = [self.chipset.REG[r] for r in regs] + for r, v in zip(regs, vals): + lines.append("{0:16s} {1:08b}b {2:02X}h".format(r, v, v)) + return lines + + +def init(transport): + log.warning("pn53x is not a driver module, use pn531, pn532, or pn533") + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) diff --git a/src/lib/nfc/clf/rcs380.py b/src/lib/nfc/clf/rcs380.py new file mode 100644 index 0000000..95fcfe6 --- /dev/null +++ b/src/lib/nfc/clf/rcs380.py @@ -0,0 +1,986 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2012, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""Driver module for contactless devices based on the Sony NFC Port-100 +chipset. The only product known to use this chipset is the PaSoRi +RC-S380. The RC-S380 connects to the host as a native USB device. + +The RC-S380 has been the first NFC Forum certified device. It supports +reading and writing of all NFC Forum tags as well as peer-to-peer +mode. In addition, the NFC Port-100 also supports card emulation Type +A and Type F Technology. A notable restriction is that peer-to-peer +active communication mode (not required for NFC Forum certification) +is not supported. + +========== ======= ============ +function support remarks +========== ======= ============ +sense_tta yes +sense_ttb yes +sense_ttf yes +sense_dep no +listen_tta yes Type F responses can not be disabled +listen_ttb no +listen_ttf yes +listen_dep yes Only passive communication mode +========== ======= ============ + +""" +import nfc.clf +from . import device + +import time +import struct +import operator +from functools import reduce +from binascii import hexlify + +import logging +log = logging.getLogger(__name__) + + +class Frame(object): + def __init__(self, data): + self._data = None + self._type = None + self._frame = None + + if data[0:3] == bytearray(b"\x00\x00\xff"): + frame = bytearray(data) + if frame == bytearray(b"\x00\x00\xff\x00\xff\x00"): + self._type = "ack" + elif frame == bytearray(b"\x00\x00\xFF\xFF\xFF"): + self._type = "err" + elif frame[3:5] == bytearray(b"\xff\xff"): + self._type = "data" + if self.type == "data": + length = struct.unpack(" 0: + data = self.send_command(0x02, data) + if data and data[0] != 0: + raise StatusError(data[0]) + + def in_comm_rf(self, data, timeout): + timeout = min((timeout + (1 if timeout > 0 else 0)) * 10, 0xFFFF) + data = self.send_command(0x04, + struct.pack("Q", data[0:8])[0] + + def set_command_type(self, command_type): + data = self.send_command(0x2A, [command_type]) + if data and data[0] != 0: + raise StatusError(data[0]) + + +class Device(device.Device): + # Device driver for the Sony NFC Port-100 chipset. + + def __init__(self, chipset, logger): + self.chipset = chipset + self.log = logger + + minor, major = self.chipset.get_firmware_version() + self._chipset_name = "NFC Port-100 v{0:x}.{1:02x}".format(major, minor) + + def close(self): + self.chipset.close() + self.chipset = None + + def mute(self): + self.chipset.switch_rf("off") + + def sense_tta(self, target): + """Sense for a Type A Target is supported for 106, 212 and 424 + kbps. However, there may not be any target that understands the + activation commands in other than 106 kbps. + + """ + log.debug("polling for NFC-A technology") + + if target.brty not in ("106A", "212A", "424A"): + message = "unsupported bitrate {0}".format(target.brty) + raise nfc.clf.UnsupportedTargetError(message) + + self.chipset.in_set_rf(target.brty) + self.chipset.in_set_protocol(self.chipset.in_set_protocol_defaults) + self.chipset.in_set_protocol(initial_guard_time=6, add_crc=0, + check_crc=0, check_parity=1, + last_byte_bit_count=7) + + sens_req = (target.sens_req if target.sens_req else + bytearray.fromhex("26")) + + try: + sens_res = self.chipset.in_comm_rf(sens_req, 30) + if len(sens_res) != 2: + return None + except CommunicationError as error: + if error != "RECEIVE_TIMEOUT_ERROR": + log.debug(error) + return None + + log.debug("rcvd SENS_RES %s", hexlify(sens_res).decode()) + + if sens_res[0] & 0x1F == 0: + log.debug("type 1 tag target found") + self.chipset.in_set_protocol(last_byte_bit_count=8, add_crc=2, + check_crc=2, type_1_tag_rrdd=2) + target = nfc.clf.RemoteTarget(target.brty, sens_res=sens_res) + if sens_res[1] & 0x0F == 0b1100: + rid_cmd = bytearray.fromhex("78 0000 00000000") + log.debug("send RID_CMD %s", hexlify(rid_cmd).decode()) + try: + target.rid_res = self.chipset.in_comm_rf(rid_cmd, 30) + except CommunicationError as error: + log.debug(error) + return None + return target + + # other than type 1 tag + try: + self.chipset.in_set_protocol(last_byte_bit_count=8, add_parity=1) + if target.sel_req: + uid = target.sel_req + if len(uid) > 4: + uid = b"\x88" + uid + if len(uid) > 8: + uid = uid[0:4] + b"\x88" + uid[4:] + self.chipset.in_set_protocol(add_crc=1, check_crc=1) + for i, sel_cmd in zip(range(0, len(uid), 4), b"\x93\x95\x97"): + sel_req = bytearray([sel_cmd, 0x70]) + uid[i:i+4] + sel_req.append(reduce(operator.xor, sel_req[2:6])) # BCC + log.debug("send SEL_REQ %s", hexlify(sel_req).decode()) + sel_res = self.chipset.in_comm_rf(sel_req, 30) + log.debug("rcvd SEL_RES %s", hexlify(sel_res).decode()) + uid = target.sel_req + else: + uid = bytearray() + for sel_cmd in b"\x93\x95\x97": + self.chipset.in_set_protocol(add_crc=0, check_crc=0) + sdd_req = bytearray([sel_cmd, 0x20]) + log.debug("send SDD_REQ %s", hexlify(sdd_req).decode()) + sdd_res = self.chipset.in_comm_rf(sdd_req, 30) + log.debug("rcvd SDD_RES %s", hexlify(sdd_res).decode()) + self.chipset.in_set_protocol(add_crc=1, check_crc=1) + sel_req = bytearray([sel_cmd, 0x70]) + sdd_res + log.debug("send SEL_REQ %s", hexlify(sel_req).decode()) + sel_res = self.chipset.in_comm_rf(sel_req, 30) + log.debug("rcvd SEL_RES %s", hexlify(sel_res).decode()) + if sel_res[0] & 0b00000100: + uid = uid + sdd_res[1:4] + else: + uid = uid + sdd_res[0:4] + break + if sel_res[0] & 0b00000100 == 0: + return nfc.clf.RemoteTarget(target.brty, sens_res=sens_res, + sel_res=sel_res, sdd_res=uid) + except CommunicationError as error: + log.debug(error) + + def sense_ttb(self, target): + """Sense for a Type B Target is supported for 106, 212 and 424 + kbps. However, there may not be any target that understands the + activation command in other than 106 kbps. + + """ + log.debug("polling for NFC-B technology") + + if target.brty not in ("106B", "212B", "424B"): + message = "unsupported bitrate {0}".format(target.brty) + raise nfc.clf.UnsupportedTargetError(message) + + self.chipset.in_set_rf(target.brty) + self.chipset.in_set_protocol(self.chipset.in_set_protocol_defaults) + self.chipset.in_set_protocol(initial_guard_time=20, add_sof=1, + check_sof=1, add_eof=1, check_eof=1) + + sensb_req = (target.sensb_req if target.sensb_req else + bytearray.fromhex("050010")) + + log.debug("send SENSB_REQ %s", hexlify(sensb_req).decode()) + try: + sensb_res = self.chipset.in_comm_rf(sensb_req, 30) + except CommunicationError as error: + if error != "RECEIVE_TIMEOUT_ERROR": + log.debug(error) + return None + + if len(sensb_res) >= 12 and sensb_res[0] == 0x50: + log.debug("rcvd SENSB_RES %s", hexlify(sensb_res).decode()) + return nfc.clf.RemoteTarget(target.brty, sensb_res=sensb_res) + + def sense_ttf(self, target): + """Sense for a Type F Target is supported for 212 and 424 kbps. + + """ + log.debug("polling for NFC-F technology") + + if target.brty not in ("212F", "424F"): + message = "unsupported bitrate {0}".format(target.brty) + raise nfc.clf.UnsupportedTargetError(message) + + self.chipset.in_set_rf(target.brty) + self.chipset.in_set_protocol(self.chipset.in_set_protocol_defaults) + self.chipset.in_set_protocol(initial_guard_time=24) + + sensf_req = (target.sensf_req if target.sensf_req else + bytearray.fromhex("00FFFF0100")) + + log.debug("send SENSF_REQ %s", hexlify(sensf_req).decode()) + try: + frame = bytearray([len(sensf_req)+1]) + sensf_req + frame = self.chipset.in_comm_rf(frame, 10) + except CommunicationError as error: + if error != "RECEIVE_TIMEOUT_ERROR": + log.debug(error) + return None + + if 18 <= len(frame) == frame[0] and frame[1] == 1: + log.debug("rcvd SENSF_RES %s", hexlify(frame[1:]).decode()) + return nfc.clf.RemoteTarget(target.brty, sensf_res=frame[1:]) + + def sense_dep(self, target): + """Sense for an active DEP Target is not supported. The device only + supports passive activation via sense_tta/sense_ttf. + + """ + message = "{device} does not support sense for active DEP Target" + raise nfc.clf.UnsupportedTargetError(message.format(device=self)) + + def listen_tta(self, target, timeout): + """Listen as Type A Target in 106 kbps. + + Restrictions: + + * It is not possible to send short frames that are required + for ACK and NAK responses. This means that a Type 2 Tag + emulation can only implement a single sector memory model. + + * It can not be avoided that the chipset responds to SENSF_REQ + commands. The driver configures the SENSF_RES response to + all zero and ignores all Type F communication but eventually + it depends on the remote device whether Type A Target + activation will still be attempted. + + """ + if not target.brty == '106A': + info = "unsupported target bitrate: %r" % target.brty + raise nfc.clf.UnsupportedTargetError(info) + + if target.rid_res: + info = "listening for type 1 tag activation is not supported" + raise nfc.clf.UnsupportedTargetError(info) + + if target.sens_res is None: + raise ValueError("sens_res is required") + if target.sdd_res is None: + raise ValueError("sdd_res is required") + if target.sel_res is None: + raise ValueError("sel_res is required") + if len(target.sens_res) != 2: + raise ValueError("sens_res must be 2 byte") + if len(target.sdd_res) != 4: + raise ValueError("sdd_res must be 4 byte") + if len(target.sel_res) != 1: + raise ValueError("sel_res must be 1 byte") + if target.sdd_res[0] != 0x08: + raise ValueError("sdd_res[0] must be 08h") + + nfca_params = target.sens_res + target.sdd_res[1:4] + target.sel_res + log.debug("nfca_params %s", hexlify(nfca_params).decode()) + + self.chipset.tg_set_rf("106A") + self.chipset.tg_set_protocol(self.chipset.tg_set_protocol_defaults) + self.chipset.tg_set_protocol(rf_off_error=False) + + time_to_return = time.time() + timeout + tg_comm_rf_args = {'mdaa': True, 'nfca_params': nfca_params} + tg_comm_rf_args['recv_timeout'] = min(int(1000 * timeout), 0xFFFF) + + def listen_tta_tt2(): + recv_timeout = tg_comm_rf_args['recv_timeout'] + while recv_timeout > 0: + log.debug("wait %d ms for Type 2 Tag activation", recv_timeout) + try: + data = self.chipset.tg_comm_rf(**tg_comm_rf_args) + except CommunicationError as error: + log.debug(error) + else: + brty = ('106A', '212F', '424F')[data[0]-11] + log.debug("%s rcvd %s", + brty, hexlify(memoryview(data)[7:]).decode()) + if brty == "106A" and data[2] & 0x03 == 3: + self.chipset.tg_set_protocol(rf_off_error=True) + return nfc.clf.LocalTarget( + "106A", sens_res=nfca_params[0:2], + sdd_res=b'\x08'+nfca_params[2:5], + sel_res=nfca_params[5:6], tt2_cmd=data[7:]) + else: + log.debug("not a 106A Type 2 Tag command") + finally: + recv_timeout = int(1000 * (time_to_return - time.time())) + tg_comm_rf_args['recv_timeout'] = recv_timeout + + def listen_tta_tt4(): + rats_cmd = rats_res = None + recv_timeout = tg_comm_rf_args['recv_timeout'] + while recv_timeout > 0: + log.debug("wait %d ms for 106A TT4 command", recv_timeout) + try: + data = self.chipset.tg_comm_rf(**tg_comm_rf_args) + tg_comm_rf_args['transmit_data'] = None + except CommunicationError as error: + tg_comm_rf_args['transmit_data'] = None + rats_cmd = rats_res = None + log.debug(error) + else: + brty = ('106A', '212F', '424F')[data[0]-11] + log.debug("%s rcvd %s", brty, + hexlify(memoryview(data)[7:]).decode()) + if brty == "106A" and data[2] == 3 and data[7] == 0xE0: + (rats_cmd, rats_res) = (data[7:], target.rats_res) + log.debug("rcvd RATS_CMD %s", + hexlify(rats_cmd).decode()) + if rats_res is None: + rats_res = bytearray.fromhex("05 78 80 70 02") + log.debug("send RATS_RES %s", + hexlify(rats_res).decode()) + tg_comm_rf_args['transmit_data'] = rats_res + elif brty == "106A" and data[7] != 0xF0 and rats_cmd: + did = rats_cmd[1] & 0x0F + cmd = data[7:] + ta_tb_tc = rats_res[2:] + ta = ta_tb_tc.pop(0) if rats_res[1] & 0x10 else None + tb = ta_tb_tc.pop(0) if rats_res[1] & 0x20 else None + tc = ta_tb_tc.pop(0) if rats_res[1] & 0x40 else None + if ta is not None: + log.debug("TA(1) = {:08b}".format(ta)) + if tb is not None: + log.debug("TB(1) = {:08b}".format(tb)) + if tc is not None: + log.debug("TC(1) = {:08b}".format(tc)) + if ta_tb_tc: + log.debug("T({}) = {}".format( + len(ta_tb_tc), hexlify(ta_tb_tc).decode())) + did_supported = tc is None or bool(tc & 0x02) + cmd_with_did = bool(cmd[0] & 0x08) + if (((cmd_with_did and did_supported and cmd[1] == did) + or (did == 0 and not cmd_with_did))): + if cmd[0] in (0xC2, 0xCA): + log.debug("rcvd S(DESELECT) %s", + hexlify(cmd).decode()) + tg_comm_rf_args['transmit_data'] = cmd + log.debug("send S(DESELECT) %s", + hexlify(cmd).decode()) + rats_cmd = rats_res = None + else: + log.debug("rcvd TT4_CMD %s", + hexlify(cmd).decode()) + self.chipset.tg_set_protocol(rf_off_error=True) + return nfc.clf.LocalTarget( + "106A", sens_res=nfca_params[0:2], + sdd_res=b'\x08'+nfca_params[2:5], + sel_res=nfca_params[5:6], tt4_cmd=cmd, + rats_cmd=rats_cmd, rats_res=rats_res) + else: + log.debug("skip TT4_CMD %s (DID)", + hexlify(cmd).decode()) + else: + log.debug("not a 106A TT4 command") + finally: + recv_timeout = int(1000 * (time_to_return - time.time())) + tg_comm_rf_args['recv_timeout'] = recv_timeout + + if target.sel_res[0] & 0x60 == 0x00: + return listen_tta_tt2() + if target.sel_res[0] & 0x20 == 0x20: + return listen_tta_tt4() + + reason = "sel_res does not indicate any tag target support" + raise nfc.clf.UnsupportedTargetError(reason) + + def listen_ttb(self, target, timeout): + """Listen as Type B Target is not supported.""" + message = "{device} does not support listen as Type A Target" + raise nfc.clf.UnsupportedTargetError(message.format(device=self)) + + def listen_ttf(self, target, timeout): + """Listen as Type F Target is supported for either 212 or 424 kbps.""" + if target.brty not in ('212F', '424F'): + info = "unsupported target bitrate: %r" % target.brty + raise nfc.clf.UnsupportedTargetError(info) + + if target.sensf_res is None: + raise ValueError("sensf_res is required") + if len(target.sensf_res) != 19: + raise ValueError("sensf_res must be 19 byte") + + self.chipset.tg_set_rf(target.brty) + self.chipset.tg_set_protocol(self.chipset.tg_set_protocol_defaults) + self.chipset.tg_set_protocol(rf_off_error=False) + + recv_timeout = min(int(1000 * timeout), 0xFFFF) + time_to_return = time.time() + timeout + transmit_data = sensf_req = sensf_res = None + + while recv_timeout > 0: + if transmit_data: + log.debug("%s send %s", target.brty, + hexlify(transmit_data).decode()) + log.debug("%s wait recv %d ms", target.brty, recv_timeout) + try: + data = self.chipset.tg_comm_rf(recv_timeout=recv_timeout, + transmit_data=transmit_data) + except CommunicationError as error: + log.debug(error) + continue + finally: + recv_timeout = int((time_to_return - time.time()) * 1E3) + transmit_data = None + + assert target.brty == ('106A', '212F', '424F')[data[0]-11] + log.debug("%s rcvd %s", target.brty, + hexlify(memoryview(data)[7:]).decode()) + + if len(data) > 7 and len(data)-7 == data[7]: + if sensf_req and data[9:17] == target.sensf_res[1:9]: + self.chipset.tg_set_protocol(rf_off_error=True) + target = nfc.clf.LocalTarget(target.brty) + target.sensf_req = sensf_req + target.sensf_res = sensf_res + target.tt3_cmd = data[8:] + return target + + if len(data) == 13 and data[7] == 6 and data[8] == 0: + (sensf_req, sensf_res) = (data[8:], target.sensf_res[:]) + if (((sensf_req[1] == 255 or sensf_req[1] == sensf_res[17]) and + (sensf_req[2] == 255 or sensf_req[2] == sensf_res[18]))): + transmit_data = sensf_res[0:17] + if sensf_req[3] == 1: + transmit_data += sensf_res[17:19] + if sensf_req[3] == 2: + transmit_data += b"\x00" + transmit_data += bytearray( + [1 << (target.brty == "424F")]) + transmit_data = bytearray([len(transmit_data)+1]) \ + + transmit_data + + def listen_dep(self, target, timeout): + log.debug("listen_dep for {0:.3f} sec".format(timeout)) + + if not target.sens_res or len(target.sens_res) != 2: + raise ValueError("sens_res is required and must be 2 byte") + if not target.sel_res or len(target.sel_res) != 1: + raise ValueError("sel_res is required and must be 1 byte") + if not target.sdd_res or len(target.sdd_res) != 4: + raise ValueError("sdd_res is required and must be 4 byte") + if not target.sensf_res or len(target.sensf_res) < 19: + raise ValueError("sensf_res is required and must be 19 byte") + if not target.atr_res or len(target.atr_res) < 17: + raise ValueError("atr_res is required and must be >= 17 byte") + + nfca_params = target.sens_res + target.sdd_res[1:4] + target.sel_res + nfcf_params = target.sensf_res[1:19] + log.debug("nfca_params %s", hexlify(nfca_params).decode()) + log.debug("nfcf_params %s", hexlify(nfcf_params).decode()) + + self.chipset.tg_set_rf("106A") + self.chipset.tg_set_protocol(self.chipset.tg_set_protocol_defaults) + self.chipset.tg_set_protocol(rf_off_error=False) + + tg_comm_rf_args = {'mdaa': True} + tg_comm_rf_args['nfca_params'] = nfca_params + tg_comm_rf_args['nfcf_params'] = nfcf_params + + recv_timeout = min(int(1000 * timeout), 0xFFFF) + time_to_return = time.time() + timeout + + while recv_timeout > 0: + tg_comm_rf_args['recv_timeout'] = recv_timeout + log.debug("wait %d ms for activation", recv_timeout) + try: + data = self.chipset.tg_comm_rf(**tg_comm_rf_args) + except CommunicationError as error: + if error != "RECEIVE_TIMEOUT_ERROR": + log.warning(error) + else: + brty = ('106A', '212F', '424F')[data[0]-11] + log.debug("%s %s", brty, hexlify(data).decode()) + if data[2] & 0x03 == 3: + data = data[7:] + break + else: + log.debug("not a passive mode activation") + recv_timeout = int(1000 * (time_to_return - time.time())) + else: + return None + + # further tg_comm_rf commands return RF_OFF_ERROR when field is gone + self.chipset.tg_set_protocol(rf_off_error=True) + + if brty == "106A" and len(data) > 1 and data[0] != 0xF0: + # We received a Type A card activation, probably because + # sel_res has indicated Type 2 or Type 4A Tag support. + target = nfc.clf.LocalTarget("106A", tt2_cmd=data[:]) + target.sens_res = nfca_params[0:2] + target.sdd_res = b'\x08' + nfca_params[2:5] + target.sel_res = nfca_params[5:6] + return target + + def verify_frame(brty, data, cmd_set): + offset = 1 if brty == "106A" else 0 + try: + if brty == "106A" and data[0] != 0xF0: + log.warning("rcvd frame has invalid start byte") + elif data[offset] != len(data) - offset: + log.warning("rcvd frame has incorrect length byte") + elif data[offset+1] != 0xD4: + log.warning("rcvd frame command byte 1 is not D4h") + elif data[offset+2] not in cmd_set: + log.warning( + "rcvd frame command byte 2 not in %r" % cmd_set) + else: + return data[offset+1:] + except (IndexError): + log.warning("rcvd frame with less than header size") + + def send_res_recv_req(brty, data, timeout): + if data: + data = (b"", b"\xF0")[brty == "106A"] + \ + bytes([len(data)+1]) + data + args = {'transmit_data': data, 'recv_timeout': timeout} + data = self.chipset.tg_comm_rf(**args)[7:] + if timeout > 0: + return verify_frame(brty, data, cmd_set=[0, 4, 6, 8, 10]) + + activation_params = nfca_params if brty == '106A' else nfcf_params + data = verify_frame(brty, data, cmd_set=[0]) + + while data and data[1] == 0: + try: + (atr_req, atr_res) = (data[:], target.atr_res) + log.debug("%s rcvd ATR_REQ %s", + brty, hexlify(atr_req).decode()) + if 16 <= len(atr_req) <= 64: + log.debug("%s send ATR_RES %s", brty, + hexlify(atr_res).decode()) + data = send_res_recv_req(brty, atr_res, 1000) + else: + log.warning("ATR_REQ must be 16 to 64 byte") + data = None + except (CommunicationError) as error: + log.warning(str(error)) + data = None + + def send_dsl_res(brty, data): + dsl_res = b"\xD5\x09" + data[2:3] + log.debug("%s send DSL_RES %s", brty, hexlify(dsl_res).decode()) + send_res_recv_req(brty, dsl_res, 0) + + def send_rls_res(brty, data): + rls_res = b"\xD5\x0B" + data[2:3] + log.debug("%s send RLS_RES %s", brty, hexlify(rls_res).decode()) + send_res_recv_req(brty, rls_res, 0) + + def send_psl_res(brty, data): + (dsi, dri) = (data[3] >> 3 & 7, data[3] & 7) + if dsi != dri: + log.error("PSL_REQ DSI != DRI is not supported") + raise CommunicationError(b'\0\0\0\0') + (psl_req, psl_res) = (data[:], b"\xD5\x05" + data[2:3]) + log.debug("%s send PSL_RES %s", brty, hexlify(psl_res).decode()) + send_res_recv_req(brty, psl_res, 0) + brty = ('106A', '212F', '424F')[dsi] + self.chipset.tg_set_rf(brty) + return brty, psl_req, psl_res + + psl_req = None + while data and data[1] in (4, 6, 8, 10): + did = atr_req[12] if atr_req[12] > 0 else None + cmd = {4: "PSL", 6: "DEP", 8: "DSL", 10: "RLS"}.get(data[1], '???') + log.debug("%s rcvd %s_REQ %s", brty, cmd, hexlify(data).decode()) + try: + if cmd == "DEP": + if did == (data[3] if data[2] >> 2 & 1 else None): + target = nfc.clf.LocalTarget(brty, dep_req=data) + target.atr_req = atr_req + if psl_req: + target.psl_req = psl_req + if activation_params == nfca_params: + target.sens_res = nfca_params[0:2] + target.sdd_res = b'\x08' + nfca_params[2:5] + target.sel_res = nfca_params[5:6] + else: + target.sensf_res = b"\x01" + nfcf_params + return target + + elif cmd == "DSL": + if did == (data[2] if len(data) > 2 else None): + return send_dsl_res(brty, data) + + elif cmd == "RLS": + if did == (data[2] if len(data) > 2 else None): + return send_rls_res(brty, data) + + elif cmd == "PSL": # pragma: no branch + if did == (data[2] if data[2] > 0 else None): + brty, psl_req, psl_res = send_psl_res(brty, data) + + log.debug("%s wait recv 1000 ms", brty) + data = send_res_recv_req(brty, None, 1000) + + except (CommunicationError) as error: + log.warning(str(error)) + return None + + def get_max_send_data_size(self, target): + return 290 + + def get_max_recv_data_size(self, target): + return 290 + + def send_cmd_recv_rsp(self, target, data, timeout): + if timeout: + timeout_msec = max(min(int(timeout * 1000), 0xFFFF), 1) + else: + timeout_msec = 0 + self.chipset.in_set_rf(target.brty_send, target.brty_recv) + self.chipset.in_set_protocol(self.chipset.in_set_protocol_defaults) + in_set_protocol_settings = {} + if target.brty_send.endswith('A'): + in_set_protocol_settings['add_parity'] = 1 + in_set_protocol_settings['check_parity'] = 1 + if target.brty_send.endswith('B'): + in_set_protocol_settings['initial_guard_time'] = 20 + in_set_protocol_settings['add_sof'] = 1 + in_set_protocol_settings['check_sof'] = 1 + in_set_protocol_settings['add_eof'] = 1 + in_set_protocol_settings['check_eof'] = 1 + try: + if ((target.brty == '106A' and target.sel_res and + target.sel_res[0] & 0x60 == 0x00)): + # Driver must check TT2 CRC to get ACK/NAK + in_set_protocol_settings['check_crc'] = 0 + self.chipset.in_set_protocol(**in_set_protocol_settings) + return self._tt2_send_cmd_recv_rsp(data, timeout_msec) + else: + self.chipset.in_set_protocol(**in_set_protocol_settings) + return self.chipset.in_comm_rf(data, timeout_msec) + except CommunicationError as error: + log.debug(error) + if error == "RECEIVE_TIMEOUT_ERROR": + raise nfc.clf.TimeoutError + raise nfc.clf.TransmissionError + + def _tt2_send_cmd_recv_rsp(self, data, timeout_msec): + # The Type2Tag implementation needs to receive the Mifare + # ACK/NAK responses but the chipset reports them as crc error + # (indistinguishable from a real crc error). We thus had to + # switch off the crc check and do it here. + data = self.chipset.in_comm_rf(data, timeout_msec) + if len(data) > 2 and self.check_crc_a(data) is False: + raise nfc.clf.TransmissionError("crc_a check error") + return data[:-2] if len(data) > 2 else data + + def send_rsp_recv_cmd(self, target, data, timeout): + assert timeout is None or timeout >= 0 + kwargs = { + 'guard_time': 500, + 'transmit_data': data, + 'recv_timeout': 0xFFFF if timeout is None else int(timeout*1E3), + } + try: + data = self.chipset.tg_comm_rf(**kwargs) + return data[7:] if data else None + except CommunicationError as error: + log.debug(error) + if error == "RF_OFF_ERROR": + raise nfc.clf.BrokenLinkError(str(error)) + if error == "RECEIVE_TIMEOUT_ERROR": + raise nfc.clf.TimeoutError(str(error)) + raise nfc.clf.TransmissionError(str(error)) + + +def init(transport): + chipset = Chipset(transport, logger=log) + device = Device(chipset, logger=log) + device._vendor_name = transport.manufacturer_name + device._device_name = transport.product_name + return device diff --git a/src/lib/nfc/clf/rcs956.py b/src/lib/nfc/clf/rcs956.py new file mode 100644 index 0000000..7dbde12 --- /dev/null +++ b/src/lib/nfc/clf/rcs956.py @@ -0,0 +1,376 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""Driver for contacless devices based on the Sony RC-S956 +chipset. Products known to use this chipset are the PaSoRi RC-S330, +RC-S360, and RC-S370. The RC-S956 connects to the host as a native USB +device. + +The RC-S956 has the same hardware architecture as the NXP PN53x +family, i.e. it has a PN512 Contactless Interface Unit (CIU) coupled +with a 80C51 microcontroller and uses the same frame structure for +host communication and mostly the same commands. However, the firmware +that runs on the 80C51 is different and the most notable difference is +a much stricter state machine. The state machine restricts allowed +commands to certain modes. While direct access to the CIU registers is +possible, some of the things that can be done with a PN53x are +unfortunately prevented by the stricter state machine. + +========== ======= ============ +function support remarks +========== ======= ============ +sense_tta yes Only Type 1 Tags up to 128 byte (Topaz-96) +sense_ttb yes ATTRIB by firmware voided with S(DESELECT) +sense_ttf yes +sense_dep yes +listen_tta yes Only DEP and Type 2 Target +listen_ttb no +listen_ttf no +listen_dep yes Only passive communication mode +========== ======= ============ + +""" +import nfc.clf +from . import pn53x + +import time + +import logging +log = logging.getLogger(__name__) + + +class Chipset(pn53x.Chipset): + CMD = { + 0x00: "Diagnose", + 0x02: "GetFirmwareVersion", + 0x04: "GetGeneralStatus", + 0x06: "ReadRegister", + 0x08: "WriteRegister", + 0x0C: "ReadGPIO", + 0x10: "SetSerialBaudrate", + 0x12: "SetParameters", + 0x16: "PowerDown", + 0x32: "RFConfiguration", + 0x58: "RFRegulationTest", + 0x18: "ResetMode", + 0x1C: "ControlLED", + 0x56: "InJumpForDEP", + 0x46: "InJumpForPSL", + 0x4A: "InListPassiveTarget", + 0x50: "InATR", + 0x4E: "InPSL", + 0x40: "InDataExchange", + 0x42: "InCommunicateThru", + 0x44: "InDeselect", + 0x52: "InRelease", + 0x54: "InSelect", + 0x8C: "TgInitTarget", + 0x92: "TgSetGeneralBytes", + 0x86: "TgGetDEPData", + 0x8E: "TgSetDEPData", + 0x94: "TgSetMetaDEPData", + 0x88: "TgGetInitiatorCommand", + 0x90: "TgResponseToInitiator", + 0x8A: "TgGetTargetStatus", + 0xA0: "CommunicateThruEX", + } + ERR = { + 0x01: "Time out, the Target has not answered", + 0x02: "Checksum error during RF communication", + 0x03: "Parity error during RF communication", + 0x04: "Incorrect collision bit position in TargetID during SDD", + 0x07: "Overflow detected by the hardware during RF communication", + 0x0A: "RF field not activated in time by active mode peer", + 0x0B: "Protocol error during RF communication", + 0x0C: "More than 260 bytes payload received in ISO-DEP chaining", + 0x0D: "Overheated - antenna drivers deactivated", + 0x10: "Size of RF response packet during SDD was more than 4 bytes", + 0x13: "Format error during RF communication or retry count exceeded", + 0x14: "Authentication A or B failed for Type-A ISO target", + 0x17: "Unmatched block number in R(ACK) from ISO Type A or B card", + 0x23: "Invalid BCC value from ISO Type A card during anticollision", + 0x25: "TgGetDEPData or TgSetDEPData executed at wrong time", + 0x26: "PowerDown command received while USB interface being used", + 0x27: "Abnormal Tg parameter in the host command packet", + 0x29: "Release from the initiator in operation as DEPTarget", + 0x2A: "PUPI information in ATQB response differs from initial value", + 0x2B: "Failure to select a deselected target", + 0x2F: "Already deselected by the initiator in operation as DEPTarget", + 0x31: "Initiator RF-OFF state detected while operating as Target", + 0x32: "Buffer overflow detected by firmware during RF communication", + 0x34: "DEP_REQ(NACK) received but DEP_RES(INF) was never returned", + 0x35: "The received data exceeds LEN in the RF packet", + 0x7f: "Invalid command syntax - received error frame", + 0xfe: "A register write operation failed", + 0xff: "No data received from executing chip command", + } + + host_command_frame_max_size = 265 + in_list_passive_target_max_target = 1 + in_list_passive_target_brty_range = (0, 1, 2, 3, 4) + + def diagnose(self, test, test_data=None): + if test == "line": + size = self.host_command_frame_max_size - 3 + data = bytearray([x & 0xFF for x in range(size)]) + return self.command(0x00, b"\x00" + data, timeout=1.0) == data + return super(Chipset, self).diagnose(test, test_data) + + def _read_register(self, data): + # Max 64 registers can be read from RCS956 + assert len(data) <= 128 + return self.command(0x06, data, timeout=0.25) + + def _write_register(self, data): + # Max 64 registers can be written to RCS956 + assert len(data) <= 192 + status = self.command(0x08, data, timeout=0.25) + if sum(status) != 0: + self.chipset_error(0xfe) + + def reset_mode(self): + """Send a Reset command to set the operation mode to 0.""" + self.command(0x18, b"\x01", timeout=0.1) + self.transport.write(Chipset.ACK) + time.sleep(0.010) + + def tg_init_target(self, mode, mifare_params, felica_params, + nfcid3t, gt, timeout): + assert type(mode) is int and mode & 0b11111101 == 0 + assert len(mifare_params) == 6 + assert len(felica_params) == 18 + assert len(nfcid3t) == 10 + + data = bytearray([mode]) + mifare_params + felica_params + nfcid3t + gt + return self.command(0x8c, data, timeout) + + +class Device(pn53x.Device): + # Device driver for Sony RC-S956 based contactless devices. + + def __init__(self, chipset, logger): + assert isinstance(chipset, Chipset) + # Reset the RCS956 state machine to Mode 0. We may have left + # it in some other mode when an error has occured. + chipset.reset_mode() + + super(Device, self).__init__(chipset, logger) + + ic, ver, rev, support = self.chipset.get_firmware_version() + self._chipset_name = "RCS956v{0:x}.{1:x}".format(ver, rev) + self.log.debug("chipset is a {0}".format(self._chipset_name)) + + self.mute() + # Set timeout for PSL_RES, ATR_RES, InDataExchange/InCommunicateThru + self.chipset.rf_configuration(0x02, b"\x0B\x0B\x0A") + self.chipset.rf_configuration(0x04, b"\x00") + self.chipset.rf_configuration(0x05, b"\x00\x00\x01") + + self.log.debug("write rf settings for 106A") + data = bytearray.fromhex("5A F4 3F 11 4D 85 61 6F 26 62 87") + self.chipset.rf_configuration(0x0A, data) + + self.chipset.set_parameters(0b00001000) + self.chipset.reset_mode() + + # Set the RFCfg value for RAM-07. RF settings in RAM-07 are + # used for initial target state. During power-up RAM-07 is + # loaded from EEPROM-07 and the RFCfg value 0xFD stored in + # EEPROM-07 for RC-S330/360 prevents passive mode activation + # at 106A. It works with the RFCfg value 0x59 stored in ROM-07 + # (Neither value makes it work in active mode). + self.chipset.write_register(0x0328, 0x59) + + def close(self): + self.mute() + super(Device, self).close() + + def mute(self): + self.chipset.reset_mode() + super(Device, self).mute() + + def sense_tta(self, target): + """Activate the RF field and probe for a Type A Target. + + The RC-S956 can discover all Type A Targets (Type 1 Tag, Type + 2 Tag, and Type 4A Tag) at 106 kbps. Due to firmware + restrictions it is not possible to read a Type 1 Tag with + dynamic memory layout (more than 128 byte memory). + + """ + target = super(Device, self).sense_tta(target) + if target and target.rid_res: + # This is a TT1 tag. Unfortunately we can only read it if + # it is a static memory tag. The RCS956 has implemented + # the same wrong command codes as PN531/2/3 and directly + # programming the CIU does not work. + if target.rid_res[0] >> 4 == 1 and target.rid_res[0] & 15 != 1: + msg = "The {device} can not read this Type 1 Tag." + self.log.warning(msg.format(device=self)) + return None + return target + + def sense_ttb(self, target): + """Activate the RF field and probe for a Type B Target. + + The RC-S956 can discover Type B Targets (Type 4B Tag) at 106 + kbps. For a Type 4B Tag the firmware automatically sends an + ATTRIB command that configures the use of DID and 64 byte + maximum frame size. The driver reverts this configuration with + a DESELECT and WUPB command to return the target prepared for + activation (which nfcpy does in the tag activation code). + + """ + return super(Device, self).sense_ttb(target, did=b'\x01') + + def sense_ttf(self, target): + """Activate the RF field and probe for a Type F Target. + + """ + return super(Device, self).sense_ttf(target) + + def sense_dep(self, target): + """Search for a DEP Target in active or passive communication mode. + + """ + # Set timeout for PSL_RES and ATR_RES + self.chipset.rf_configuration(0x02, b"\x0B\x0B\x0A") + return super(Device, self).sense_dep(target) + + def listen_tta(self, target, timeout): + """Listen *timeout* seconds for a Type A activation at 106 kbps. The + ``sens_res``, ``sdd_res``, and ``sel_res`` response data must + be provided and ``sdd_res`` must be a 4 byte UID that starts + with ``08h``. Depending on ``sel_res`` an activation may + return a target with ``tt2_cmd`` or ``atr_req`` attribute. A + Type 4A Tag activation is not supported. + + """ + if target.sel_res and target.sel_res[0] & 0x20: + info = "{device} does not support listen as Type 4A Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + return super(Device, self).listen_tta(target, timeout) + + def listen_ttb(self, target, timeout): + """Listen as Type B Target is not supported.""" + info = "{device} does not support listen as Type B Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_ttf(self, target, timeout): + """Listen as Type F Target is not supported.""" + info = "{device} does not support listen as Type F Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_dep(self, target, timeout): + """Listen *timeout* seconds to become initialized as a DEP Target. + + The RC-S956 can be set to listen as a DEP Target for passive + communication mode. Target active communication mode is + disabled by the driver due to performance issues. It is also + not possible to fully control the ATR_RES response, only the + response waiting time (TO byte of ATR_RES) and the general + bytes can be set by the driver. Because the TO value must be + set before calling the hardware listen function, it can not be + different for the Type A of Type F passive initalization (the + driver uses the higher value if they are different). + + """ + # The RCS956 internal state machine must be in Mode 0 before + # we enter the listen phase. Also the RFConfiguration command + # for setting the TO parameter won't work in any other mode. + self.chipset.reset_mode() + + # Set the WaitForSelected bit in CIU_FelNFC2 register to + # prevent active mode activation. Target active mode is not + # really working with this device. + self.chipset.write_register("CIU_FelNFC2", 0x80) + + # We can not send ATR_RES as as a regular response but must + # use TgSetGeneralBytes to advance the chipset state machine + # to mode 3. Thus the ATR_RES is mostly determined by the + # firmware, we can only control the TO parameter for RWT, but + # must do it before the actual listen. + to = target.atr_res[15] & 0x0F + self.chipset.rf_configuration(0x82, bytearray([to, 2, to])) + + # Disable automatic ATR_RES transmission. This must be done + # all again because the chipset reactivates the setting after + # ATR_RES was once send in TgSetGeneralBytes. + self.chipset.set_parameters(0b00001000) + + # Now we can use the generic pn53x implementation + return super(Device, self).listen_dep(target, timeout) + + def _init_as_target(self, mode, tta_params, ttf_params, timeout): + nfcid3t = ttf_params[0:8] + b"\x00\x00" + args = (mode & 0xFE, tta_params, ttf_params, nfcid3t, b'', timeout) + return self.chipset.tg_init_target(*args) + + def _send_atr_response(self, atr_res, timeout): + # Before ATR_RES the device is in Mode 2 which does not allow + # the use of TgResponseToInitiator. To send the ATR_RES we + # must use TgSetGeneralBytes and can control only the general + # bytes and TO which we've set in _listen_dep(). We now copy + # the DID value from atr_req to atr_res but this will likely + # have no effect on the actual response. The hope is that the + # firmware will do the same when sending ATR_RES and we tell + # the truth to the caller. + self.log.debug("calling TgSetGeneralBytes to send ATR_RES") + self.chipset.tg_set_general_bytes(atr_res[17:]) + return self.chipset.tg_get_initiator_command(timeout) + + def _tt1_send_cmd_recv_rsp(self, data, timeout): + # Special handling for Tag Type 1 (Jewel/Topaz) card commands. + + if data[0] in (0x00, 0x01, 0x1A, 0x53, 0x72): + # RALL, READ, WRITE-NE, WRITE-E, RID are properly + # implemented by firmware. + return self.chipset.in_data_exchange(data, timeout)[0] + + # The other commands can not be executed. The workaround found + # for PN531, PN532 and PN533 fails with RCS956. While it is + # possible to properly send a TT1 command and the tag answers + # as expected, there is no way to get the response data from + # the CIU FIFO. For whatever reason the FIFO is empty, maybe + # the firmware constantly polls for new data and just removes + # it. That the response data was received can be guessed from + # the fact that the CIU Control register shows has the + # RxLastBits field set to exactly the correct number of valid + # bits in the last byte (when parity check is disabled, + # i.e. the FIFO contains one more bit for each received byte. + self.log.debug("tt1 command can not be send with this hardware ") + raise nfc.clf.TransmissionError("tt1 command can not be send") + + +def init(transport): + # Write ack to see if we can talk to the device. This raises + # IOError(EACCES) if it's claimed by some other process. + transport.write(Chipset.ACK) + + chipset = Chipset(transport, logger=log) + device = Device(chipset, logger=log) + + device._vendor_name = transport.manufacturer_name + device._device_name = transport.product_name + if device._device_name is None: + device._device_name = "RC-S330" + + return device diff --git a/src/lib/nfc/clf/transport.py b/src/lib/nfc/clf/transport.py new file mode 100644 index 0000000..b0eef8a --- /dev/null +++ b/src/lib/nfc/clf/transport.py @@ -0,0 +1,345 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2012, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +# +# Transport layer for host to reader communication. +# +import os +import re +import errno +from binascii import hexlify + +if not os.getenv("READTHEDOCS"): # pragma: no cover + try: + import usb1 as libusb + except ImportError: # pragma: no cover + raise ImportError("missing usb1 module, try 'pip install libusb1'") + +try: + import serial + import serial.tools.list_ports +except ImportError: # pragma: no cover + raise ImportError("missing serial module, try 'pip install pyserial'") + +try: + import termios +except ImportError: # pragma: no cover + assert os.name != 'posix' + +import logging +log = logging.getLogger(__name__) + +PATH = re.compile(r'^([a-z]+)(?::|)([a-zA-Z0-9-]+|)(?::|)([a-zA-Z0-9]+|)$') + + +class TTY(object): + TYPE = "TTY" + + @classmethod + def find(cls, path): + if not (path.startswith("tty") or path.startswith("com")): + return + + match = PATH.match(path) + + if match and match.group(1) == "tty": + if re.match(r'^(S|ACM|AMA|USB)\d+$', match.group(2)): + TTYS = re.compile(r'^tty{}$'.format(match.group(2))) + glob = False + elif re.match(r'^(S|ACM|AMA|USB)$', match.group(2)): + TTYS = re.compile(r'^tty{}\d+$'.format(match.group(2))) + glob = True + elif re.match(r'^usbserial-\w+$', match.group(2)): + TTYS = re.compile(r'^cu\.{}$'.format(match.group(2))) + glob = False + elif re.match(r'^usbserial$', match.group(2)): + TTYS = re.compile(r'^cu\.usbserial-.*$') + glob = True + elif re.match(r'^.+$', match.group(2)): + TTYS = re.compile(r'^{}$'.format(match.group(2))) + glob = False + else: + TTYS = re.compile(r'^(tty(S|ACM|AMA|USB)\d+|cu\.usbserial.*)$') + glob = True + + log.debug(TTYS.pattern) + ttys = [fn for fn in os.listdir('/dev') if TTYS.match(fn)] + + if len(ttys) > 0: + # Sort ttys with custom function to correctly order numbers. + ttys.sort(key=lambda item: (len(item), item)) + log.debug('check: ' + ' '.join('/dev/' + tty for tty in ttys)) + + # Eliminate tty nodes that are not physically present or + # inaccessible by the current user. Propagate IOError when + # path designated exactly one device, otherwise just log. + for i, tty in enumerate(ttys): + try: + termios.tcgetattr(open('/dev/%s' % tty)) + ttys[i] = '/dev/%s' % tty + except termios.error: + pass + except IOError as error: + log.debug(error) + if not glob: + raise error + + ttys = [tty for tty in ttys if tty.startswith('/dev/')] + log.debug('avail: %s', ' '.join([tty for tty in ttys])) + return ttys, match.group(3), glob + + if match and match.group(1) == "com": + if re.match(r'^COM\d+$', match.group(2)): + return [match.group(2)], match.group(3), False + if re.match(r'^\d+$', match.group(2)): + return ["COM" + match.group(2)], match.group(3), False + if re.match(r'^$', match.group(2)): + ports = [p[0] for p in serial.tools.list_ports.comports()] + log.debug('serial ports: %s', ' '.join(ports)) + return ports, match.group(3), True + log.error("invalid port in 'com' path: %r", match.group(2)) + + @property + def manufacturer_name(self): + return None + + @property + def product_name(self): + return None + + def __init__(self, port=None): + self.tty = None + self.open(port) + + def open(self, port, baudrate=115200): + self.close() + self.tty = serial.Serial(port, baudrate, timeout=0.05) + + @property + def port(self): + return self.tty.port if self.tty else '' + + @property + def baudrate(self): + return self.tty.baudrate if self.tty else 0 + + @baudrate.setter + def baudrate(self, value): + if self.tty: + self.tty.baudrate = value + + def read(self, timeout): + if self.tty is not None: + self.tty.timeout = max(timeout/1E3, 0.05) + frame = bytearray(self.tty.read(6)) + if frame is None or len(frame) == 0: + raise IOError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT)) + if frame.startswith(b"\x00\x00\xff\x00\xff\x00"): + log.log(logging.DEBUG-1, "<<< %s", hexlify(frame).decode()) + return frame + LEN = frame[3] + if LEN == 0xFF: + frame += self.tty.read(3) + LEN = frame[5] << 8 | frame[6] + frame += self.tty.read(LEN + 1) + log.log(logging.DEBUG-1, "<<< %s", hexlify(frame).decode()) + return frame + + def write(self, frame): + if self.tty is not None: + log.log(logging.DEBUG-1, ">>> %s", hexlify(frame).decode()) + self.tty.flushInput() + try: + self.tty.write(frame) + except serial.SerialTimeoutException: + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + def close(self): + if self.tty is not None: + self.tty.flushOutput() + self.tty.close() + self.tty = None + + +class USB(object): + TYPE = "USB" + + @classmethod + def find(cls, path): + if not path.startswith("usb"): + return + + log.debug("using libusb-{0}.{1}.{2}".format(*libusb.getVersion()[0:3])) + + usb_or_none = re.compile(r'^(usb|)$') + usb_vid_pid = re.compile(r'^usb(:[0-9a-fA-F]{4})(:[0-9a-fA-F]{4})?$') + usb_bus_dev = re.compile(r'^usb(:[0-9]{1,3})(:[0-9]{1,3})?$') + match = None + + for regex in (usb_vid_pid, usb_bus_dev, usb_or_none): + m = regex.match(path) + if m is not None: + log.debug("path matches {0!r}".format(regex.pattern)) + if regex is usb_vid_pid: + match = [int(s.strip(':'), 16) for s in m.groups() if s] + match = dict(zip(['vid', 'pid'], match)) + if regex is usb_bus_dev: + match = [int(s.strip(':'), 10) for s in m.groups() if s] + match = dict(zip(['bus', 'adr'], match)) + if regex is usb_or_none: + match = dict() + break + else: + return None + + with libusb.USBContext() as context: + devices = context.getDeviceList(skip_on_error=True) + vid, pid = match.get('vid'), match.get('pid') + bus, dev = match.get('bus'), match.get('adr') + if vid is not None: + devices = [d for d in devices if d.getVendorID() == vid] + if pid is not None: + devices = [d for d in devices if d.getProductID() == pid] + if bus is not None: + devices = [d for d in devices if d.getBusNumber() == bus] + if dev is not None: + devices = [d for d in devices if d.getDeviceAddress() == dev] + return [(d.getVendorID(), d.getProductID(), d.getBusNumber(), + d.getDeviceAddress()) for d in devices] + + def __init__(self, usb_bus, dev_adr): + self.context = libusb.USBContext() + self.open(usb_bus, dev_adr) + + def __del__(self): + self.close() + if self.context: # pragma: no branch + self.context.exit() + + def open(self, usb_bus, dev_adr): + self.usb_dev = None + self.usb_out = None + self.usb_inp = None + + for dev in self.context.getDeviceList(skip_on_error=True): + if ((dev.getBusNumber() == usb_bus and + dev.getDeviceAddress() == dev_adr)): + break + else: + log.error("no device {0} on bus {1}".format(dev_adr, usb_bus)) + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + try: + first_setting = next(dev.iterSettings()) + except StopIteration: + log.error("no usb configuration settings, please replug device") + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + def transfer_type(x): + return x & libusb.TRANSFER_TYPE_MASK + + def endpoint_dir(x): + return x & libusb.ENDPOINT_DIR_MASK + + for endpoint in first_setting.iterEndpoints(): + ep_addr = endpoint.getAddress() + ep_attr = endpoint.getAttributes() + if transfer_type(ep_attr) == libusb.TRANSFER_TYPE_BULK: + if endpoint_dir(ep_addr) == libusb.ENDPOINT_IN: + if not self.usb_inp: + self.usb_inp = endpoint + if endpoint_dir(ep_addr) == libusb.ENDPOINT_OUT: + if not self.usb_out: + self.usb_out = endpoint + + if not (self.usb_inp and self.usb_out): + log.error("no bulk endpoints for read and write") + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + try: + # workaround the PN533's buggy USB implementation + self._manufacturer_name = dev.getManufacturer() + self._product_name = dev.getProduct() + except libusb.USBErrorIO: + self._manufacturer_name = None + self._product_name = None + + try: + self.usb_dev = dev.open() + self.usb_dev.claimInterface(0) + except libusb.USBErrorAccess: + raise IOError(errno.EACCES, os.strerror(errno.EACCES)) + except libusb.USBErrorBusy: + raise IOError(errno.EBUSY, os.strerror(errno.EBUSY)) + except libusb.USBErrorNoDevice: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + + def close(self): + if self.usb_dev: + self.usb_dev.close() + self.usb_dev = None + self.usb_out = None + self.usb_inp = None + + @property + def manufacturer_name(self): + return self._manufacturer_name + + @property + def product_name(self): + return self._product_name + + def read(self, timeout=0): + if self.usb_inp is not None: + try: + ep_addr = self.usb_inp.getAddress() + frame = self.usb_dev.bulkRead(ep_addr, 300, timeout) + except libusb.USBErrorTimeout: + raise IOError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT)) + except libusb.USBErrorNoDevice: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + except libusb.USBError as error: + log.error("%r", error) + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + if len(frame) == 0: + log.error("bulk read returned zero data") + raise IOError(errno.EIO, os.strerror(errno.EIO)) + + frame = bytearray(frame) + log.log(logging.DEBUG-1, "<<< %s", hexlify(frame).decode()) + return frame + + def write(self, frame, timeout=0): + if self.usb_out is not None: + log.log(logging.DEBUG-1, ">>> %s", hexlify(frame).decode()) + try: + ep_addr = self.usb_out.getAddress() + self.usb_dev.bulkWrite(ep_addr, bytes(frame), timeout) + if len(frame) % self.usb_out.getMaxPacketSize() == 0: + self.usb_dev.bulkWrite(ep_addr, b'', timeout) + except libusb.USBErrorTimeout: + raise IOError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT)) + except libusb.USBErrorNoDevice: + raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) + except libusb.USBError as error: + log.error("%r", error) + raise IOError(errno.EIO, os.strerror(errno.EIO)) diff --git a/src/lib/nfc/clf/udp.py b/src/lib/nfc/clf/udp.py new file mode 100644 index 0000000..dbb629b --- /dev/null +++ b/src/lib/nfc/clf/udp.py @@ -0,0 +1,577 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2012, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +"""Driver module for simulated contactless communication over +UDP/IP. It can be activated with the device path ``udp::`` +where the optional *host* may be the IP address or name of the node +where the targeted communication partner is listening on *port*. The +default values for *host* and *port* are ``localhost:54321``. + +The driver implements almost all communication modes, with the current +exception of active communication mode data exchange protocol. + +========== ======= ============ +function support remarks +========== ======= ============ +sense_tta yes +sense_ttb yes +sense_ttf yes +sense_dep no +listen_tta yes +listen_ttb yes +listen_ttf yes +listen_dep yes +========== ======= ============ + +""" +import nfc.clf + +import time +import errno +import socket +import select +import operator +from functools import reduce +from binascii import hexlify, unhexlify + +import logging +log = logging.getLogger(__name__) + + +class Device(nfc.clf.device.Device): + def __init__(self, host, port): + host = socket.gethostbyname(host) + host, port = socket.getnameinfo((host, port), socket.NI_NUMERICHOST) + self.addr = (host, int(port)) + self._path = "%s:%s" % (host, port) + self.socket = None + self._create_socket() + + def close(self): + self.mute() + + def mute(self): + if self.socket: + # send RFOFF when socket port != listen port + if self.socket.getsockname()[1] != self.addr[1] and self.rcvd_data: + self._send_data("RFOFF", b"", self.addr) + self.socket.close() + self.socket = None + + def sense_tta(self, target): + self._create_socket() + + log.debug("sense_tta for %s on %s:%d", target, *self.addr) + + if target.brty not in ("106A", "212A", "424A"): + message = "unsupported bitrate {0}".format(target.brty) + raise nfc.clf.UnsupportedTargetError(message) + + sens_req = (target.sens_req if target.sens_req else + bytearray.fromhex("26")) + + log.debug("send SENS_REQ %s", hexlify(sens_req).decode()) + try: + self._send_data(target.brty, sens_req, self.addr) + brty, sens_res, addr = self._recv_data(1.0, target.brty) + except nfc.clf.TimeoutError: + return None + + log.debug("rcvd SENS_RES %s", hexlify(sens_res).decode()) + + if sens_res[0] & 0x1F == 0: + log.debug("type 1 tag target found") + target = nfc.clf.RemoteTarget(target.brty, _addr=addr) + target.sens_res = sens_res + if sens_res[1] & 0x0F == 0b1100: + rid_cmd = bytearray.fromhex("78 0000 00000000") + log.debug("send RID_CMD %s", hexlify(rid_cmd).decode()) + try: + self._send_data(brty, rid_cmd, self.addr) + brty, rid_res, addr = self._recv_data(1.0, brty) + target.rid_res = rid_res + except nfc.clf.CommunicationError as error: + log.debug(error) + return None + return target + + # other than type 1 tag + try: + if target.sel_req: + uid = target.sel_req + if len(uid) > 4: + uid = b"\x88" + uid + if len(uid) > 8: + uid = uid[0:4] + b"\x88" + uid[4:] + for i, sel_cmd in zip(range(0, len(uid), 4), b"\x93\x95\x97"): + sel_req = bytearray([sel_cmd, 0x70]) + uid[i:i+4] + sel_req.append(reduce(operator.xor, sel_req[2:6])) # BCC + log.debug("send SEL_REQ {}".format( + hexlify(sel_req).decode())) + self._send_data(brty, sel_req, addr) + brty, sel_res, addr = self._recv_data(0.5, brty) + log.debug("rcvd SEL_RES {}".format( + hexlify(sel_res).decode())) + uid = target.sel_req + else: + uid = bytearray() + for sel_cmd in b"\x93\x95\x97": + sdd_req = bytearray([sel_cmd, 0x20]) + log.debug("send SDD_REQ {}".format( + hexlify(sdd_req).decode())) + self._send_data(brty, sdd_req, addr) + brty, sdd_res, addr = self._recv_data(0.5, brty) + log.debug("rcvd SDD_RES {}".format( + hexlify(sdd_res).decode())) + sel_req = bytearray([sel_cmd, 0x70]) + sdd_res + log.debug("send SEL_REQ {}".format( + hexlify(sel_req).decode())) + self._send_data(brty, sel_req, addr) + brty, sel_res, addr = self._recv_data(0.5, brty) + log.debug("rcvd SEL_RES {}".format( + hexlify(sel_res).decode())) + if sel_res[0] & 0b00000100: + uid = uid + sdd_res[1:4] + else: + uid = uid + sdd_res[0:4] + break + if sel_res[0] & 0b00000100 == 0: + target = nfc.clf.RemoteTarget(target.brty, _addr=addr) + target.sens_res = sens_res + target.sel_res = sel_res + target.sdd_res = uid + return target + except nfc.clf.CommunicationError as error: + log.debug(error) + + def sense_ttb(self, target): + self._create_socket() + + if target.brty not in ("106B", "212B", "424B"): + message = "unsupported bitrate {0}".format(target.brty) + raise nfc.clf.UnsupportedTargetError(message) + + sensb_req = (target.sensb_req if target.sensb_req else + bytearray.fromhex("050010")) + + log.debug("send SENSB_REQ %s", hexlify(sensb_req).decode()) + try: + self._send_data(target.brty, sensb_req, self.addr) + brty, sensb_res, addr = self._recv_data(1.0, target.brty) + except nfc.clf.CommunicationError: + return None + + if len(sensb_res) >= 12 and sensb_res[0] == 0x50: + log.debug("rcvd SENSB_RES %s", hexlify(sensb_res).decode()) + return nfc.clf.RemoteTarget(brty, sensb_res=sensb_res, _addr=addr) + + def sense_ttf(self, target): + self._create_socket() + + log.debug("sense_ttf for %s on %s:%d", target, *self.addr) + + if target.brty not in ("212F", "424F"): + message = "unsupported bitrate {0}".format(target.brty) + raise nfc.clf.UnsupportedTargetError(message) + + if not target.sensf_req: + sensf_req = bytearray.fromhex("0600FFFF0100") + else: + sensf_req = bytearray([len(target.sensf_req)+1]) + target.sensf_req + + log.debug("send SENSF_REQ {}".format( + hexlify(memoryview(sensf_req)[1:]).decode())) + try: + self._send_data(target.brty, sensf_req, self.addr) + brty, data, addr = self._recv_data(1.0, target.brty) + except nfc.clf.CommunicationError: + return None + + if len(data) >= 18 and data[0] == len(data) and data[1] == 1: + log.debug("rcvd SENSF_RES %s", hexlify(data[1:]).decode()) + return nfc.clf.RemoteTarget(brty, sensf_res=data[1:], _addr=addr) + + def sense_dep(self, target): + info = "{device} does not support sense for active DEP Target" + raise nfc.clf.UnsupportedTargetError(info.format(device=self)) + + def listen_tta(self, target, timeout): + self._create_socket() + + log.debug("listen_tta for %.3f seconds on %s:%d", timeout, *self.addr) + + time_to_return = time.time() + timeout + if not self._bind_socket(time_to_return): + log.debug("failed to bind socket") + return None + + log.debug("wait for data on socket %s:%d", *self.socket.getsockname()) + return self._listen_tta(target, time_to_return) + + def _listen_tta(self, target, time_to_return, init=None): + sdd_res = bytearray(target.sdd_res) + if len(sdd_res) > 4: + sdd_res.insert(0, 0x88) + if len(sdd_res) > 8: + sdd_res.insert(4, 0x88) + sdd_res.insert(4, reduce(operator.xor, sdd_res[0:4])) + if len(sdd_res) > 5: + sdd_res.insert(9, reduce(operator.xor, sdd_res[5:9])) + if len(sdd_res) > 10: + sdd_res.insert(14, reduce(operator.xor, sdd_res[10:14])) + sel_res = bytearray([target.sel_res[0] & 0b11111011]) + + while time.time() < time_to_return: + if init is None: + wait = max(0.5, time_to_return - time.time()) + try: + brty, data, addr = self._recv_data(wait, target.brty) + except nfc.clf.TimeoutError: + return None + except nfc.clf.CommunicationError: + continue + else: + (brty, data, addr), init = init, None + if data == b"\x26": + log.debug("rcvd SENS_REQ %s", hexlify(data).decode()) + sens_res = target.sens_res + log.debug("send SENS_RES %s", hexlify(sens_res).decode()) + self._send_data(brty, sens_res, addr) + elif data == b"\x93\x20": + log.debug("rcvd SDD_REQ CL1 %s", hexlify(data).decode()) + log.debug("send SDD_RES CL1 %s", + hexlify(sdd_res[0:5]).decode()) + self._send_data(brty, sdd_res[0:5], addr) + elif data == b"\x95\x20" and len(sdd_res) > 5: + log.debug("rcvd SDD_REQ CL2 %s", hexlify(data).decode()) + log.debug("send SDD_RES CL2 %s", + hexlify(sdd_res[5:10]).decode()) + self._send_data(brty, sdd_res[5:10], addr) + elif data == b"\x97\x20" and len(sdd_res) > 10: + log.debug("rcvd SDD_REQ CL3 %s", hexlify(data).decode()) + log.debug("send SDD_RES CL3 %s", + hexlify(sdd_res[10:15]).decode()) + self._send_data(brty, sdd_res[10:15], addr) + elif data == b"\x93\x70" + sdd_res[0:5]: + log.debug("rcvd SEL_REQ Cl1 %s", hexlify(data).decode()) + sel_res[0] = (sel_res[0] & 0xFB) | (len(sdd_res) > 5) << 2 + log.debug("send SEL_RES %s", hexlify(sel_res).decode()) + self._send_data(brty, sel_res, addr) + elif data == b"\x95\x70" + sdd_res[5:10]: + log.debug("rcvd SEL_REQ CL2 %s", hexlify(data).decode()) + sel_res[0] = (sel_res[0] & 0xFB) | (len(sdd_res) > 10) << 2 + log.debug("send SEL_RES %s", hexlify(sel_res).decode()) + self._send_data(brty, sel_res, addr) + elif data == b"\x95\x70" + sdd_res[10:15]: + log.debug("rcvd SEL_REQ CL3 %s", hexlify(data).decode()) + sel_res[0] = (sel_res[0] & 0xFB) | (len(sdd_res) > 15) << 2 + log.debug("send SEL_RES %s", hexlify(sel_res).decode()) + self._send_data(brty, sel_res, addr) + elif sel_res[0] & 0b00000100 == 0: + target = nfc.clf.LocalTarget( + brty, _addr=addr, sens_res=target.sens_res, + sdd_res=target.sdd_res, sel_res=target.sel_res) + if ((data[0] == 0xF0 and len(data) >= 18 and + data[1] == len(data)-1 and data[2:4] == b"\xD4\x00")): + target.atr_req = data[2:] + elif data[0] == 0xE0: + target.tt4_cmd = data[:] + else: + target.tt2_cmd = data[:] + return target + + def listen_ttb(self, target, timeout): + self._create_socket() + + log.debug("listen_ttb for %.3f seconds on %s:%d", timeout, *self.addr) + + time_to_return = time.time() + timeout + if not self._bind_socket(time_to_return): + log.debug("failed to bind socket") + return None + + assert target.sensb_res and len(target.sensb_res) >= 12 + log.debug("wait for data on socket %s:%d", *self.socket.getsockname()) + + while time.time() < time_to_return: + wait = max(0.5, time_to_return - time.time()) + try: + brty, data, addr = self._recv_data(wait, target.brty) + except nfc.clf.TimeoutError: + return None + except nfc.clf.CommunicationError: + continue + if data and len(data) == 3 and data.startswith(b'\x05'): + req = "ALLB_REQ" if data[1] & 0x08 else "SENSB_REQ" + sensb_req = data + log.debug("rcvd %s %s", req, hexlify(sensb_req).decode()) + log.debug("send SENSB_RES %s", + hexlify(target.sensb_res).decode()) + self._send_data(brty, target.sensb_res, addr) + brty, data, addr = self._recv_data(wait, target.brty) + return nfc.clf.LocalTarget(brty, sensb_req=sensb_req, + sensb_res=target.sensb_res, + tt4_cmd=data, _addr=addr) + + def listen_ttf(self, target, timeout): + self._create_socket() + + log.debug("listen_ttf for %.3f seconds on %s:%d", timeout, *self.addr) + + time_to_return = time.time() + timeout + if not self._bind_socket(time_to_return): + log.debug("failed to bind socket") + return None + + log.debug("wait for data on socket %s:%d", *self.socket.getsockname()) + return self._listen_ttf(target, time_to_return) + + def _listen_ttf(self, target, time_to_return, init=None): + sensf_req = sensf_res = None + while time.time() < time_to_return: + if init is None: + wait = max(0.5, time_to_return - time.time()) + try: + brty, data, addr = self._recv_data(wait, target.brty) + except nfc.clf.TimeoutError: + return None + except nfc.clf.CommunicationError: + continue + else: + (brty, data, addr), init = init, None + if data and len(data) == data[0]: + if data.startswith(b"\x06\x00"): + (sensf_req, sensf_res) = (data[1:], target.sensf_res[:]) + if (((sensf_req[1] == 255 or + sensf_req[1] == sensf_res[17]) and + (sensf_req[2] == 255 or + sensf_req[2] == sensf_res[18]))): + data = sensf_res[0:17] + if sensf_req[3] == 1: + data += sensf_res[17:19] + if sensf_req[3] == 2: + data += bytearray( + [0x00, 1 << (target.brty == "424F")]) + data = bytearray([len(data)+1]) + data + self._send_data(brty, data, addr) + else: + sensf_req = sensf_res = None + elif sensf_req and sensf_res: + if data[2:10] == target.sensf_res[1:9]: + target = nfc.clf.LocalTarget(brty, _addr=addr) + target.sensf_req = sensf_req + target.sensf_res = sensf_res + target.tt3_cmd = data[1:] + return target + if data[1:11] == b'\xD4\x00' + target.sensf_res[1:9]: + target = nfc.clf.LocalTarget(brty, _addr=addr) + target.sensf_req = sensf_req + target.sensf_res = sensf_res + target.atr_req = data[1:] + return target + + def listen_dep(self, target, timeout): + self._create_socket() + + log.debug("listen_dep for %.3f seconds on %s:%d", timeout, *self.addr) + assert target.sensf_res is not None + assert target.sens_res is not None + assert target.sdd_res is not None + assert target.sel_res is not None + assert target.atr_res is not None + assert len(target.sensf_res) == 19 + assert len(target.sens_res) == 2 + assert len(target.sdd_res) == 4 + assert len(target.sel_res) == 1 + assert len(target.atr_res) >= 17 and len(target.atr_res) <= 64 + + time_to_return = time.time() + timeout + if not self._bind_socket(time_to_return): + log.debug("failed to bind socket") + return None + + log.debug("wait for data on socket %s:%d", *self.socket.getsockname()) + atr_res = bytearray(target.atr_res) + + while time.time() < time_to_return: + wait = max(0, time_to_return - time.time()) + try: + result = self._recv_data(wait, '106A', '212F', '424F') + brty, data, addr = result + except nfc.clf.CommunicationError: + return None + + target.brty = brty + if brty == '106A': + if data == b"\x26": + init = (brty, data, addr) + target = self._listen_tta(target, time_to_return, init) + elif (len(data) >= 18 and data[1] == len(data)-1 and + data[0] == 0xF0 and data[2:4] == b'\xD4\x00'): + target = nfc.clf.LocalTarget( + brty, atr_res=target.atr_res, atr_req=data[2:]) + elif brty in ('212F', '424F') and data[0] == len(data): + if data.startswith(b'\x06\x00'): + init = (brty, data, addr) + target = self._listen_ttf(target, time_to_return, init) + elif len(data) >= 17 and data[1:3] == b'\xD4\x00': + target = nfc.clf.LocalTarget( + brty, atr_res=target.atr_res, atr_req=data[1:]) + + if target and target.atr_req: + target.atr_res = atr_res + log.debug("rcvd ATR_REQ %s", hexlify(target.atr_req).decode()) + log.debug("send ATR_RES %s", hexlify(target.atr_res).decode()) + data = bytearray([len(atr_res) + 1]) + atr_res + if brty == '106A': + data.insert(0, 0xF0) + self._send_data(brty, data, addr) + brty, data, addr = self._recv_data(wait, brty) + try: + if brty == '106A': + assert data.pop(0) == 0xF0 + assert len(data) == data.pop(0) + except AssertionError: + return None + if data.startswith(b'\xD4\x04'): + target.psl_req = data[:] + target.psl_res = b'\xD5\x05' + target.psl_req[2:3] + log.debug("rcvd PSL_REQ %s", + hexlify(target.psl_req).decode()) + log.debug("send PSL_RES %s", + hexlify(target.psl_res).decode()) + data = bytearray([len(target.psl_res) + 1]) \ + + target.psl_res + if brty == '106A': + data.insert(0, 0xF0) + self._send_data(brty, data, addr) + brty = ('106A', '212F', '424F')[target.psl_req[3] >> 3 & 7] + target.brty, data, addr = self._recv_data(wait, brty) + try: + if brty == '106A': + assert data.pop(0) == 0xF0 + assert len(data) == data.pop(0) + except AssertionError: + return None + if data.startswith(b'\xD4\x08'): + log.debug("rcvd DSL_REQ %s", hexlify(data).decode()) + data = b'\xD5\x09' + data[2:3] + log.debug("send DSL_RES %s", hexlify(data).decode()) + data = bytearray([len(data) + 1]) + data + if brty == '106A': + data.insert(0, 0xF0) + self._send_data(brty, data, addr) + return None + if data.startswith(b'\xD4\x0A'): + log.debug("rcvd RLS_REQ %s", hexlify(data).decode()) + data = b'\xD5\x0B' + data[2:3] + log.debug("send RLS_RES %s", hexlify(data).decode()) + data = bytearray([len(data) + 1]) + data + if brty == '106A': + data.insert(0, 0xF0) + self._send_data(brty, data, addr) + return None + if data.startswith(b'\xD4\x06'): + target.dep_req = data[:] + return target + return None + + def send_cmd_recv_rsp(self, target, data, timeout): + # send data, data should normally not be None for the Initiator + if data is not None: + self._send_data(target.brty, data, target._addr) + + # receive response data unless the timeout is zero + if timeout > 0: + brty, data, addr = self._recv_data(timeout, target.brty) + return data + + def send_rsp_recv_cmd(self, target, data, timeout): + # send data, data may be none as target keeps silence on error + if data is not None: + self._send_data(target.brty, data, target._addr) + + # recv response data unless the timeout is zero + if timeout is None or timeout > 0: + brty, data, addr = self._recv_data(timeout, target.brty) + return data + + def get_max_send_data_size(self, target): + return 290 + + def get_max_recv_data_size(self, target): + return 290 + + def _create_socket(self): + if self.socket is None: + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.sent_data = self.rcvd_data = 0 + + def _bind_socket(self, time_to_return): + addr = ('0.0.0.0', self.addr[1]) + while time.time() < time_to_return: + log.debug("trying to bind socket to %s:%d", *addr) + try: + self.socket.bind(addr) + return True + except socket.error as error: + log.debug("bind failed with %s", error) + if error.errno == errno.EADDRINUSE: + return False + else: + raise error + + def _send_data(self, brty, data, addr): + data = (b"%s %s" % (brty.encode('latin'), hexlify(data))).strip() + log.log(logging.DEBUG-1, ">>> %s to %s:%s", data.decode(), *addr) + ret = self.socket.sendto(data, addr) + if ret != len(data): + raise nfc.clf.TransmissionError("failed to send data") + self.sent_data += len(data) + + def _recv_data(self, timeout, *brty_list): + time_to_return = None if timeout is None else (time.time() + timeout) + while timeout is None or time.time() < time_to_return: + wait = None if timeout is None else (time_to_return - time.time()) + if len(select.select([self.socket], [], [], wait)[0]) == 1: + data, addr = self.socket.recvfrom(1024) + log.log(logging.DEBUG-1, "<<< %s from %s:%d", data, *addr) + if data.startswith(b"RFOFF"): + raise nfc.clf.BrokenLinkError("RFOFF") + try: + brty, data = data.split() + except ValueError: + raise nfc.clf.TransmissionError("no data") + brty = brty.decode("ascii") + data = bytearray(unhexlify(data)) + self.rcvd_data += len(data) + if brty in brty_list: + return brty, data, addr + raise nfc.clf.TimeoutError("no data received") + + +def init(host, port): + import platform + device = Device(host, port) + device._vendor_name = platform.uname()[0] + device._device_name = "IP-Stack" + device._chipset_name = "UDP" + return device diff --git a/src/lib/nfc/dep.py b/src/lib/nfc/dep.py new file mode 100644 index 0000000..6d93bde --- /dev/null +++ b/src/lib/nfc/dep.py @@ -0,0 +1,895 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import src.lib.nfc.clf + +import os +import time +import collections +import struct +from binascii import hexlify + +import logging +log = logging.getLogger(__name__) + + +class DataExchangeProtocol(object): + class Counter(object): + def __init__(self): + self.sent = collections.defaultdict(int) + self.rcvd = collections.defaultdict(int) + + @property + def sent_count(self): + return sum(self.sent.values()) + + @property + def rcvd_count(self): + return sum(self.rcvd.values()) + + def __str__(self): + s = "sent/rcvd {0}/{1}".format(self.sent_count, self.rcvd_count) + for name in sorted(set(list(self.sent.keys()) + + list(self.rcvd.keys()))): + s += " {name} {sent}/{rcvd}".format( + name=name, sent=self.sent[name], rcvd=self.rcvd[name]) + return s + + def __init__(self, clf): + self.pcnt = DataExchangeProtocol.Counter() + self.clf = clf + self.gbi = b"" + self.gbt = b"" + + @property + def general_bytes(self): + """The general bytes received with the ATR exchange""" + pass + + @property + def role(self): + """Role in DEP communication, either 'Target' or 'Initiator'""" + pass + + +class Initiator(DataExchangeProtocol): + ROLE = "Initiator" + + def __init__(self, clf): + DataExchangeProtocol.__init__(self, clf) + self.target = None + self.miu = None # maximum information unit size + self.did = None # dep device identifier + self.nad = None # dep node address + self.gbt = None # general bytes from target + self.pni = None # dep packet number information + self.rwt = None # target response waiting time + self._acm = None # active communication mode flag + + @property + def role(self): + return "Initiator" + + @property + def general_bytes(self): + return self.gbt + + @property + def acm(self): + return bool(self._acm) + + def __str__(self): + msg = "NFC-DEP Initiator {brty} {mode} mode MIU={miu} RWT={rwt:.6f}" + return msg.format(brty=self.target.brty, miu=self.miu, rwt=self.rwt, + mode=("passive", "active")[self.acm]) + + def activate(self, target=None, **options): + """Activate DEP communication with a target.""" + log.debug("initiator options: {0}".format(options)) + + self.did = options.get('did', None) + self.nad = options.get('nad', None) + self.gbi = options.get('gbi', b'')[0:48] + self.brs = min(max(0, options.get('brs', 2)), 2) + self.lri = min(max(0, options.get('lri', 3)), 3) + if self._acm is None or 'acm' in options: + self._acm = bool(options.get('acm', True)) + + assert self.did is None or 0 <= self.did <= 255 + assert self.nad is None or 0 <= self.nad <= 255 + + ppi = (self.lri << 4) | (bool(self.gbi) << 1) | int(bool(self.nad)) + did = 0 if self.did is None else self.did + atr_req = ATR_REQ(os.urandom(10), did, 0, 0, ppi, self.gbi) + psl_req = PSL_REQ(did, (0, 9, 18)[self.brs], self.lri) + atr_res = psl_res = None + self.target = target + + if self.target is None and self.acm is True: + log.debug("searching active communication mode target at 106A") + tg = nfc.clf.RemoteTarget("106A", atr_req=atr_req.encode()) + try: + self.target = self.clf.sense(tg, iterations=2, interval=0.1) + except nfc.clf.UnsupportedTargetError: + self._acm = False + except nfc.clf.CommunicationError: + pass + else: + if self.target: + atr_res = ATR_RES.decode(self.target.atr_res) + else: + self._acm = None + + if self.target is None: + log.debug("searching passive communication mode target at 106A") + target = nfc.clf.RemoteTarget("106A") + target = self.clf.sense(target, iterations=2, interval=0.1) + if target and target.sel_res and bool(target.sel_res[0] & 0x40): + self.target = target + + if self.target is None and self.brs > 0: + log.debug("searching passive communication mode target at 212F") + target = nfc.clf.RemoteTarget("212F", sensf_req=b'\0\xFF\xFF\0\0') + target = self.clf.sense(target, iterations=2, interval=0.1) + if target and target.sensf_res.startswith(b'\1\1\xFE'): + atr_req.nfcid3 = target.sensf_res[1:9] + b'ST' + self.target = target + + if self.target and self.target.atr_res is None: + try: + atr_res = self.send_req_recv_res(atr_req, 1.0) + except nfc.clf.CommunicationError: + pass + if atr_res is None: + log.debug("NFC-DEP Attribute Request failed") + return None + + if self.target and atr_res: + if self.brs > ('106A', '212F', '424F').index(self.target.brty): + try: + psl_res = self.send_req_recv_res(psl_req, 0.1) + except nfc.clf.CommunicationError: + pass + if psl_res is None: + log.debug("NFC-DEP Parameter Selection failed") + return None + self.target.brty = ('212F', '424F')[self.brs-1] + + self.rwt = (4096/13.56E6 + * 2**(atr_res.wt if atr_res.wt < 15 else 14)) + self.miu = (atr_res.lr-3 - int(self.did is not None) + - int(self.nad is not None)) + self.gbt = atr_res.gb + self.pni = 0 + + log.info("running as " + str(self)) + return self.gbt + + def deactivate(self, release=True): + log.debug("deactivate {0}".format(self)) + req = RLS_REQ(self.did) if release else DSL_REQ(self.did) + try: + res = self.send_req_recv_res(req, 0.1) + except nfc.clf.CommunicationError: + return + else: + if res.did != req.did: + log.error("target returned wrong DID in " + res.PDU_NAME) + finally: + log.debug("packets {0}".format(self.pcnt)) + + def exchange(self, send_data, timeout): + def INF(pni, data, more, did, nad): + pdu_type = (DEP_REQ.LastInformation, DEP_REQ.MoreInformation)[more] + pfb = DEP_REQ.PFB(pdu_type, nad is not None, did is not None, pni) + return DEP_REQ(pfb, did, nad, data) + + def ACK(pni, did, nad): + pdu_type = DEP_REQ.PositiveAck + pfb = DEP_REQ.PFB(pdu_type, nad is not None, did is not None, pni) + return DEP_REQ(pfb, did, nad, data=None) + + def RTOX(rtox, did, nad): + if not 0 < rtox < 60: + error = "NFC-DEP RTOX must be in range 1 to 59" + raise nfc.clf.ProtocolError(error) + pdu_type = DEP_REQ.TimeoutExtension + pfb = DEP_REQ.PFB(pdu_type, nad is not None, did is not None, 0) + return DEP_REQ(pfb, did, nad, data=bytearray([rtox])) + + # log.debug("dep raw >> %s", hexlify(send_data).decode()) + send_data = bytearray(send_data) + + while send_data: + data = send_data[0:self.miu] + del send_data[0:self.miu] + req = INF(self.pni, data, bool(send_data), self.did, self.nad) + res = self.send_dep_req_recv_dep_res(req, self.rwt, timeout) + if res.pfb.fmt == DEP_RES.TimeoutExtension: + for i in range(3): + req = RTOX(res.data[0], self.did, self.nad) + rwt = res.data[0] * self.rwt + log.warning("target requested %.3f sec more time", rwt) + res = self.send_dep_req_recv_dep_res(req, rwt, timeout) + if res.pfb.fmt != DEP_RES.TimeoutExtension: + break + else: + log.error("too many timeout extension requests") + raise nfc.clf.TimeoutError("timeout extension") + if res.pfb.fmt == DEP_RES.PositiveAck: + if not send_data: + error = "unexpected or out-of-sequence NFC-DEP ACK PDU" + raise nfc.clf.ProtocolError(error) + if res.pfb.pni != self.pni: + raise nfc.clf.ProtocolError("wrong NFC-DEP packet number") + self.pni = (self.pni + 1) & 0x3 + + if ((res.pfb.fmt != DEP_RES.LastInformation and + res.pfb.fmt != DEP_RES.MoreInformation)): + error = "expected NFC-DEP INF PDU after sending" + raise nfc.clf.ProtocolError(error) + + recv_data = res.data + + while res.pfb.fmt == DEP_RES.MoreInformation: + req = ACK(self.pni, self.did, self.nad) + res = self.send_dep_req_recv_dep_res(req, self.rwt, timeout) + if res.pfb.fmt == DEP_RES.TimeoutExtension: + for i in range(3): + req = RTOX(res.data[0], self.did, self.nad) + rwt = res.data[0] * self.rwt + log.warning("target requested %.3f sec more time", rwt) + res = self.send_dep_req_recv_dep_res(req, rwt, timeout) + if res.pfb.fmt != DEP_RES.TimeoutExtension: + break + else: + log.error("too many timeout extension requests") + raise nfc.clf.TimeoutError("timeout extension") + if ((res.pfb.fmt != DEP_RES.LastInformation and + res.pfb.fmt != DEP_RES.MoreInformation)): + error = "NFC-DEP chaining not continued after ACK" + raise nfc.clf.ProtocolError(error) + if res.pfb.pni != self.pni: + raise nfc.clf.ProtocolError("wrong NFC-DEP packet number") + recv_data += res.data + self.pni = (self.pni + 1) & 0x3 + + # log.debug("dep raw << %s", hexlify(recv_data).decode()) + return recv_data + + def send_dep_req_recv_dep_res(self, req, rwt, timeout): + def NAK(pni, did, nad): + pdu_type = DEP_REQ.NegativeAck + pfb = DEP_REQ.PFB( + pdu_type, nad is not None, did is not None, self.pni) + return DEP_REQ(pfb, did, nad, data=None) + + def ATN(): + pdu_type = DEP_REQ.Attention + pfb = DEP_REQ.PFB(pdu_type, nad=False, did=False, pni=0) + return DEP_REQ(pfb, did=None, nad=None, data=None) + + def request_attention(self, n_retry_atn, rwt, deadline): + req = ATN() + for i in range(n_retry_atn): + timeout = min(rwt, deadline - time.time()) + if timeout <= 0: + raise nfc.clf.TimeoutError + try: + res = self.send_req_recv_res(req, timeout) + except nfc.clf.CommunicationError: + continue + if res.pfb.fmt == DEP_RES.TimeoutExtension: + error = "received NFC-DEP RTOX response to NACK or ATN" + raise nfc.clf.ProtocolError(error) + if res.pfb.fmt != DEP_RES.Attention: + error = "expected NFC-DEP Attention response" + raise nfc.clf.ProtocolError(error) + return + error = "unrecoverable NFC-DEP error in attention request" + raise nfc.clf.ProtocolError(error) + + def request_retransmission(self, n_retry_nak, rwt, deadline): + req = NAK(self.pni, self.did, self.nad) + for i in range(n_retry_nak): + timeout = min(rwt, deadline - time.time()) + if timeout <= 0: + raise nfc.clf.TimeoutError + try: + res = self.send_req_recv_res(req, timeout) + except nfc.clf.CommunicationError: + continue + if res.pfb.fmt == DEP_RES.TimeoutExtension: + error = "received NFC-DEP RTOX response to NACK or ATN" + raise nfc.clf.ProtocolError(error) + expected = (DEP_RES.LastInformation, DEP_RES.MoreInformation) + if res.pfb.fmt not in expected: + error = "unrecoverable NFC-DEP transmission error" + raise nfc.clf.ProtocolError(error) + return res + error = "unrecoverable NFC-DEP error in retransmission request" + raise nfc.clf.ProtocolError(error) + + if rwt > timeout: + text = "response waiting time %.3f exceeds the timeout of %.3f sec" + log.warning(text, rwt, timeout) + + deadline = time.time() + timeout + while True: + timeout = min(rwt, deadline - time.time()) + if timeout <= 0: + raise nfc.clf.TimeoutError() + try: + res = self.send_req_recv_res(req, timeout) + break + except nfc.clf.TimeoutError: + request_attention(self, 2, rwt, deadline) + continue + except nfc.clf.TransmissionError: + res = request_retransmission(self, 2, rwt, deadline) + break + + if res.pfb.fmt == DEP_RES.NegativeAck: + error = "received NFC-DEP NACK PDU from Target" + raise nfc.clf.ProtocolError(error) + + return res + + def send_req_recv_res(self, req, timeout): + log.debug(">> {0}".format(req)) + pcnt_key = req.PDU_NAME[:3] + if isinstance(req, DEP_REQ): + pcnt_key += " " + req.pfb.FMT_NAME + self.pcnt.sent[pcnt_key] += 1 + + cmd = self.encode_frame(req) + rsp = self.clf.exchange(cmd, timeout) + res = self.decode_frame(rsp) + if res.PDU_NAME[0:3] != req.PDU_NAME[0:3]: + raise nfc.clf.ProtocolError("invalid response for " + req.PDU_NAME) + + log.debug("<< {0}".format(res)) + pcnt_key = res.PDU_NAME[:3] + if isinstance(res, DEP_RES): + pcnt_key += " " + res.pfb.FMT_NAME + self.pcnt.rcvd[pcnt_key] += 1 + return res + + def encode_frame(self, packet): + frame = packet.encode() + frame = struct.pack("B", len(frame) + 1) + frame + if self.target.brty == '106A': + frame = b'\xF0' + frame + return bytearray(frame) + + def decode_frame(self, frame): + if self.target.brty == '106A' and frame.pop(0) != 0xF0: + error = "first NFC-DEP frame byte must be F0h for 106A" + raise nfc.clf.ProtocolError(error) + if len(frame) != frame.pop(0): + error = "NFC-DEP frame length byte must be data length + 1" + raise nfc.clf.ProtocolError(error) + if len(frame) < 2: + error = "NFC-DEP frame length byte must be from 3 to 255" + raise nfc.clf.TransmissionError(error) + if frame[0] != 0xD5 or frame[1] not in (1, 5, 7, 9, 11): + raise nfc.clf.ProtocolError("invalid NFC-DEP response code") + res_name = {1: 'ATR', 5: 'PSL', 7: 'DEP', 9: 'DSL', 11: 'RLS'} + return eval(res_name[frame[1]] + "_RES").decode(frame) + + +class Target(DataExchangeProtocol): + def __init__(self, clf): + DataExchangeProtocol.__init__(self, clf) + self.miu = None # maximum information unit size + self.did = None # dep device identifier + self.nad = None # dep node address + self.gbi = None # general bytes from initiator + self.pni = None # dep packet number information + self.rwt = None # target response waiting time + + @property + def role(self): + return "Target" + + @property + def general_bytes(self): + return self.gbi + + def __str__(self): + msg = "NFC-DEP Target {brty} {mode} mode MIU={miu} RWT={rwt:.6f}" + return msg.format(brty=self.target.brty, miu=self.miu, rwt=self.rwt, + mode=("passive", "active")[self.acm]) + + def activate(self, timeout=None, **options): + """Activate DEP communication as a target.""" + + if timeout is None: + timeout = 1.0 + gbt = options.get('gbt', b'')[0:47] + lrt = min(max(0, options.get('lrt', 3)), 3) + rwt = min(max(0, options.get('rwt', 8)), 14) + + pp = (lrt << 4) | (bool(gbt) << 1) | int(bool(self.nad)) + nfcid3t = bytearray.fromhex("01FE") + os.urandom(6) + b"ST" + atr_res = ATR_RES(nfcid3t, 0, 0, 0, rwt, pp, gbt) + atr_res = atr_res.encode() + + target = nfc.clf.LocalTarget(atr_res=atr_res) + target.sens_res = bytearray.fromhex("0101") + target.sdd_res = bytearray.fromhex("08") + os.urandom(3) + target.sel_res = bytearray.fromhex("40") + target.sensf_res = bytearray.fromhex("01") + nfcid3t[0:8] + target.sensf_res += bytearray.fromhex("00000000 00000000 FFFF") + + target = self.clf.listen(target, timeout) + + if target and target.atr_req and target.dep_req: + log.debug("activated as " + str(target)) + + atr_req = ATR_REQ.decode(target.atr_req) + self.lrt = lrt + self.gbt = gbt + self.gbi = atr_req.gb + self.miu = atr_req.lr - 3 + self.rwt = 4096/13.56E6 * pow(2, rwt) + self.did = atr_req.did if atr_req.did > 0 else None + self.acm = not (target.sens_res or target.sensf_res) + self.cmd = bytearray( + struct.pack("B", len(target.dep_req)+1) + target.dep_req) + if target.brty == "106A": + self.cmd = bytearray(b"\xF0" + self.cmd) + self.target = target + + self.pcnt.rcvd["ATR"] += 1 + self.pcnt.sent["ATR"] += 1 + log.info("running as " + str(self)) + + return self.gbi + + def deactivate(self, data=bytearray()): + try: + log.debug("deactivate {0}".format(self)) + self._deactivate(data) + finally: + log.debug("packets {0}".format(self.pcnt)) + + def _deactivate(self, data): + def INF(pni, data, did, nad): + pdu_type = DEP_RES.LastInformation + pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, pni) + return DEP_RES(pfb, did, nad, data) + + def ATN(did, nad): + pdu_type = DEP_RES.Attention + pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, 0) + return DEP_RES(pfb, did, nad, data=None) + + res = None + deadline = time.time() + 1.0 + while time.time() < deadline: # pragma: no branch + try: + req = self.send_res_recv_req(res, deadline) + except nfc.clf.CommunicationError: + return + if req is None: + return + if req.did == self.did: + if type(req) in (DSL_REQ, RLS_REQ): + RES = DSL_RES if type(req) == DSL_REQ else RLS_RES + try: + self.send_res_recv_req(RES(self.did), 0) + except nfc.clf.CommunicationError: + pass + return + if type(req) == DEP_REQ: + if req.pfb.fmt == DEP_REQ.Attention: + res = ATN(self.did, self.nad) + else: + res = INF(req.pfb.pni, data, self.did, self.nad) + continue + res = None + + def exchange(self, send_data, timeout): + def INF(pni, data, more, did, nad): + pdu_type = (DEP_RES.LastInformation, DEP_RES.MoreInformation)[more] + pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, pni) + return DEP_RES(pfb, did, nad, data) + + def ACK(pni, did, nad): + pdu_type = DEP_RES.PositiveAck + pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, pni) + return DEP_RES(pfb, did, nad, data=None) + + if send_data is not None and len(send_data) == 0: + raise ValueError("send_data must not be empty") + + deadline = time.time() + timeout + + if self.cmd is not None: + # first command frame received in activate is injected in + # send_res_recv_req and self.cmd then set to None + assert send_data is None, "send_data should be None on first call" + req = self.send_dep_res_recv_dep_req(None, deadline) + self.pni = 0 + else: + send_data = bytearray(send_data) + while send_data: + data = send_data[0:self.miu] + more = len(send_data) > self.miu + res = INF(self.pni, data, more, self.did, self.nad) + req = self.send_dep_res_recv_dep_req(res, deadline) + if req is None: + return None + if more: + if req.pfb.fmt is not DEP_REQ.PositiveAck: + error = "expected ACK in NFC-DEP chaining" + raise nfc.clf.ProtocolError(error) + self.pni = (self.pni + 1) & 0x3 + if req.pfb.pni != self.pni: + raise nfc.clf.ProtocolError("wrong NFC-DEP packet number") + del send_data[0:self.miu] + + recv_data = bytearray() + while req.pfb.fmt == DEP_REQ.MoreInformation: + recv_data += req.data + res = ACK(self.pni, self.did, self.nad) + req = self.send_dep_res_recv_dep_req(res, deadline) + if req is None: + return None + self.pni = (self.pni + 1) & 0x3 + if req.pfb.pni != self.pni: + raise nfc.clf.ProtocolError("wrong NFC-DEP packet number") + + recv_data += req.data + return recv_data + + def send_timeout_extension(self, rtox): + def RTOX(rtox, did, nad): + pdu_type = DEP_RES.TimeoutExtension + pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, 0) + return DEP_RES(pfb, did, nad, data=bytearray([rtox])) + + res = RTOX(rtox, self.did, self.nad) + req = self.send_dep_res_recv_dep_req(res, deadline=time.time()+1) + if type(req) == DEP_REQ and req.pfb.fmt == DEP_REQ.TimeoutExtension: + return req.data[0] & 0x3F + + def send_dep_res_recv_dep_req(self, dep_res, deadline): + def ATN(did, nad): + pdu_type = DEP_RES.Attention + pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, 0) + return DEP_RES(pfb, did, nad, data=None) + + res = dep_res + dep_req = None + while dep_req is None: + req = self.send_res_recv_req(res, deadline) + if req is None: + return None + elif req.did != self.did: + log.debug("ignore non-matching device identifier") + res = None + elif type(req) == DSL_REQ: + return self.send_res_recv_req(DSL_RES(self.did), 0) + elif type(req) == RLS_REQ: + return self.send_res_recv_req(RLS_RES(self.did), 0) + elif type(req) == DEP_REQ: + if req.pfb.fmt == DEP_REQ.Attention: + res = ATN(self.did, self.nad) + elif req.pfb.fmt == DEP_REQ.NegativeAck: + res = dep_res + elif req.pfb.fmt == DEP_REQ.TimeoutExtension: + dep_req = req + elif req.pfb.pni == self.pni: + res = dep_res + else: + dep_req = req + else: + log.debug("invalid command in data exchange context") + res = None + return dep_req + + def send_res_recv_req(self, res, deadline): + frame = None + + if self.cmd is not None: + # first command is received in activate + frame, self.cmd = self.cmd, None + else: + if res is not None: + log.debug(">> {0}".format(res)) + pcnt_key = res.PDU_NAME[:3] + if isinstance(res, DEP_RES): + pcnt_key += " " + res.pfb.FMT_NAME + self.pcnt.sent[pcnt_key] += 1 + frame = self.encode_frame(res) + while True: + timeout = deadline-time.time() if deadline > time.time() else 0 + try: + frame = self.clf.exchange(frame, timeout=timeout) + except nfc.clf.TransmissionError: + frame = None + else: + break + + if frame: + req = self.decode_frame(frame) + log.debug("<< {0}".format(req)) + pcnt_key = req.PDU_NAME[:3] + if isinstance(req, DEP_REQ): + pcnt_key += " " + req.pfb.FMT_NAME + self.pcnt.rcvd[pcnt_key] += 1 + return req + + def encode_frame(self, packet): + frame = packet.encode() + frame = struct.pack("B", len(frame) + 1) + frame + if self.target.brty == '106A': + frame = b'\xF0' + frame + return bytearray(frame) + + def decode_frame(self, frame): + if self.target.brty == '106A' and frame.pop(0) != 0xF0: + error = "first NFC-DEP frame byte must be F0h for 106A" + raise nfc.clf.ProtocolError(error) + if len(frame) != frame.pop(0): + error = "NFC-DEP frame length byte must be data length + 1" + raise nfc.clf.ProtocolError(error) + if len(frame) < 2: + error = "NFC-DEP frame length byte must be from 3 to 255" + raise nfc.clf.TransmissionError(error) + if frame[0] != 0xD4 or frame[1] not in (0, 4, 6, 8, 10): + raise nfc.clf.ProtocolError("invalid NFC-DEP command code") + req_name = {0: 'ATR', 4: 'PSL', 6: 'DEP', 8: 'DSL', 10: 'RLS'} + return eval(req_name[frame[1]] + "_REQ").decode(frame) + + +# +# Data Exchange Protocol Data Units +# +class ATR_REQ_RES(object): + def __str__(self): + nfcid3, gb = [hexlify(ba).decode() for ba in [self.nfcid3, self.gb]] + return self.PDU_SHOW.format(self=self, nfcid3=nfcid3, gb=gb) + + @property + def lr(self): + return (64, 128, 192, 254)[(self.pp >> 4) & 0x3] + + +class ATR_REQ(ATR_REQ_RES): + PDU_CODE = bytearray(b'\xD4\x00') + PDU_NAME = 'ATR-REQ' + PDU_SHOW = "{self.PDU_NAME} NFCID3={nfcid3} DID={self.did:02x} "\ + "BS={self.bs:02x} BR={self.br:02x} PP={self.pp:02x} GB={gb}" + + def __init__(self, nfcid3, did, bs, br, pp, gb): + self.nfcid3, self.did, self.bs, self.br, self.pp, self.gb = \ + nfcid3, did, bs, br, pp, gb + + def __len__(self): + return 16 + len(self.gb) + + @staticmethod + def decode(data): + if data.startswith(ATR_REQ.PDU_CODE): + nfcid3, (did, bs, br, pp) = data[2:12], data[12:16] + gb = data[16:] if pp & 0x02 else bytearray() + return ATR_REQ(nfcid3, did, bs, br, pp, gb) + + def encode(self): + data = ATR_REQ.PDU_CODE + self.nfcid3 + data.extend([self.did, self.bs, self.br, self.pp]) + return data + self.gb + + +class ATR_RES(ATR_REQ_RES): + PDU_CODE = bytearray(b'\xD5\x01') + PDU_NAME = 'ATR-RES' + PDU_SHOW = "{self.PDU_NAME} NFCID3={nfcid3} DID={self.did:02x} "\ + "BS={self.bs:02x} BR={self.br:02x} TO={self.to:02x} "\ + "PP={self.pp:02x} GB={gb}" + + def __init__(self, nfcid3, did, bs, br, to, pp, gb): + self.nfcid3, self.did, self.bs, self.br, self.to, self.pp, self.gb = \ + nfcid3, did, bs, br, to, pp, gb + + def __len__(self): + return 17 + len(self.gb) + + @staticmethod + def decode(data): + if data.startswith(ATR_RES.PDU_CODE): + nfcid3, (did, bs, br, to, pp) = data[2:12], data[12:17] + gb = data[17:] if pp & 0x02 else bytearray() + return ATR_RES(nfcid3, did, bs, br, to, pp, gb) + + def encode(self): + data = ATR_RES.PDU_CODE + self.nfcid3 + data.extend([self.did, self.bs, self.br, self.to, self.pp]) + return data + self.gb + + @property + def wt(self): + return self.to & 0x0F + + +class PSL_REQ_RES(object): + def __str__(self): + return self.PDU_SHOW.format(name=self.PDU_NAME, self=self) + + @classmethod + def decode(cls, data): + if data.startswith(cls.PDU_CODE): + try: + return cls(*data[2:]) + except TypeError: + errstr = "invalid format of the " + cls.PDU_NAME + raise nfc.clf.ProtocolError(errstr) + + +class PSL_REQ(PSL_REQ_RES): + PDU_CODE = bytearray(b'\xD4\x04') + PDU_NAME = 'PSL-REQ' + PDU_SHOW = "{name} DID={self.did:02x} BRS={self.brs:02x} " \ + "FSL={self.fsl:02x}" + + def __init__(self, did, brs, fsl): + self.did, self.brs, self.fsl = did if did else 0, brs, fsl + + def encode(self): + return PSL_REQ.PDU_CODE + bytearray([self.did, self.brs, self.fsl]) + + @property + def dsi(self): + return self.brs >> 3 & 0x07 + + @property + def dri(self): + return self.brs & 0x07 + + @property + def lr(self): + return (64, 128, 192, 254)[self.fsl & 0x03] + + +class PSL_RES(PSL_REQ_RES): + PDU_CODE = bytearray(b'\xD5\x05') + PDU_NAME = 'PSL-RES' + PDU_SHOW = "{name} DID={self.did:02x}" + + def __init__(self, did): + self.did = did + + def encode(self): + return PSL_RES.PDU_CODE + bytearray([self.did]) + + +class DEP_REQ_RES(object): + PDU_SHOW = "{self.PDU_NAME} {self.pfb.FMT_NAME} PNI={self.pfb.pni} "\ + "DID={self.did} NAD={self.nad} DATA={data}" + + class PFB: + def __init__(self, fmt, nad, did, pni): + self.fmt, self.nad, self.did, self.pni = fmt, nad, did, pni + + @property + def FMT_NAME(self): + return {0: "INF", 1: "I++", 4: "ACK", 5: "NAK", 8: "ATN", + 9: "TOX"}.get(self.fmt, "{0:04b}".format(self.fmt)) + + @property + def type(self): return self.fmt + + LastInformation, MoreInformation, PositiveAck, NegativeAck,\ + Attention, TimeoutExtension = (0, 1, 4, 5, 8, 9) + + def __init__(self, pfb, did, nad, data): + self.pfb, self.did, self.nad = pfb, did, nad + self.data = bytearray() if data is None else data + + def __str__(self): + data = hexlify(self.data).decode() + return self.PDU_SHOW.format(self=self, data=data) + + def bytes(self): + data = hexlify(self.data) + return self.PDU_SHOW.format(self=self, data=data) + + @classmethod + def decode(cls, data): + if data.startswith(cls.PDU_CODE): + del data[0:2] + try: + pfb = data.pop(0) + pfb = cls.PFB(pfb >> 4, bool(pfb & 8), bool(pfb & 4), pfb & 3) + did = data.pop(0) if pfb.did else None + nad = data.pop(0) if pfb.nad else None + except IndexError: + errstr = "invalid format of the " + cls.PDU_NAME + raise nfc.clf.ProtocolError(errstr) + return cls(pfb, did, nad, data) + + def encode(self): + pfb = self.pfb + pfb = (pfb.fmt << 4) | (pfb.nad << 3) | (pfb.did << 2) | (pfb.pni) + data = self.PDU_CODE + struct.pack("B", pfb) + if self.pfb.did: + data.append(self.did) + if self.pfb.nad: + data.append(self.nad) + return data + self.data + + +class DEP_REQ(DEP_REQ_RES): + PDU_CODE = bytearray(b'\xD4\x06') + PDU_NAME = 'DEP-REQ' + + +class DEP_RES(DEP_REQ_RES): + PDU_CODE = bytearray(b'\xD5\x07') + PDU_NAME = 'DEP-RES' + + +class DSL_REQ_RES(object): + def __init__(self, did): + self.did = did + + def __str__(self): + return "{0} DID={1}".format(self.PDU_NAME, self.did) + + @classmethod + def decode(cls, data): + if data.startswith(cls.PDU_CODE): + if len(data) > 3: + errstr = "invalid format of the " + cls.PDU_NAME + raise nfc.clf.ProtocolError(errstr) + return cls(data[2] if len(data) == 3 else None) + + def encode(self): + return self.PDU_CODE + (b"" + if self.did is None + else struct.pack("B", self.did)) + + +class DSL_REQ(DSL_REQ_RES): + PDU_CODE = bytearray(b'\xD4\x08') + PDU_NAME = 'DSL-REQ' + + +class DSL_RES(DSL_REQ_RES): + PDU_CODE = bytearray(b'\xD5\x09') + PDU_NAME = 'DSL-RES' + + +class RLS_REQ_RES(DSL_REQ_RES): + pass + + +class RLS_REQ(RLS_REQ_RES): + PDU_CODE = bytearray(b'\xD4\x0A') + PDU_NAME = 'RLS-REQ' + + +class RLS_RES(RLS_REQ_RES): + PDU_CODE = bytearray(b'\xD5\x0B') + PDU_NAME = 'RLS-RES' diff --git a/src/lib/nfc/handover/__init__.py b/src/lib/nfc/handover/__init__.py new file mode 100644 index 0000000..e887b4d --- /dev/null +++ b/src/lib/nfc/handover/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2012 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +""" +The nfc.handover module implements the NFC Forum Connection Handover +1.2 protocol as a server and client class that simplify realization of +handover selector and requester functionality. + +""" +from src.lib.nfc.handover.server import HandoverServer # noqa: F401 +from src.lib.nfc.handover.client import HandoverClient # noqa: F401 diff --git a/src/lib/nfc/handover/client.py b/src/lib/nfc/handover/client.py new file mode 100644 index 0000000..2715e76 --- /dev/null +++ b/src/lib/nfc/handover/client.py @@ -0,0 +1,118 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +# +# Negotiated Connection Handover - Client Base Class +# +import binascii +import logging +import time +import ndef +import src.lib.nfc + + +log = logging.getLogger(__name__) + + +class HandoverClient(object): + """ NFC Forum Connection Handover client + """ + def __init__(self, llc): + self.socket = None + self.llc = llc + + def connect(self, recv_miu=248, recv_buf=2): + """Connect to the remote handover server if available. Raises + :exc:`nfc.llcp.ConnectRefused` if the remote device does not + have a handover service or the service does not accept any + more connections.""" + socket = nfc.llcp.Socket(self.llc, nfc.llcp.DATA_LINK_CONNECTION) + socket.setsockopt(nfc.llcp.SO_RCVBUF, recv_buf) + socket.setsockopt(nfc.llcp.SO_RCVMIU, recv_miu) + socket.connect("urn:nfc:sn:handover") + server = socket.getpeername() + log.debug("handover client connected to remote sap {0}".format(server)) + self.socket = socket + + def close(self): + """Disconnect from the remote handover server.""" + if self.socket: + self.socket.close() + self.socket = None + + def send_records(self, records): + """Send handover request message records to the remote server.""" + log.debug("sending '{0}' message".format(records[0].type)) + try: + octets = b''.join(ndef.message_encoder(records)) + except ndef.EncodeError as error: + log.error(repr(error)) + else: + return self.send_octets(octets) + + def send_octets(self, octets): + log.debug(">>> %s", binascii.hexlify(octets).decode()) + miu = self.socket.getsockopt(nfc.llcp.SO_SNDMIU) + while len(octets) > 0: + if self.socket.send(octets[0:miu]): + octets = octets[miu:] + else: + break + return len(octets) == 0 + + def recv_records(self, timeout=None): + """Receive a handover select message from the remote server.""" + octets = self.recv_octets(timeout) + records = list(ndef.message_decoder(octets, 'relax')) if octets else [] + if records and records[0].type == "urn:nfc:wkt:Hs": + log.debug("received '{0}' message".format(records[0].type)) + return list(ndef.message_decoder(octets, 'relax')) + else: + log.error("received invalid message %s", binascii.hexlify(octets)) + return [] + + def recv_octets(self, timeout=None): + octets = bytearray() + started = time.time() + while self.socket.poll("recv", timeout): + try: + octets += self.socket.recv() + except TypeError: + log.debug("data link connection closed") + return b'' # recv() returned None + try: + list(ndef.message_decoder(octets, 'strict', {})) + log.debug("<<< %s", binascii.hexlify(octets).decode()) + return bytes(octets) + except ndef.DecodeError: + log.debug("message is incomplete (%d byte)", len(octets)) + if timeout: + timeout -= time.time() - started + started = time.time() + log.debug("%.3f seconds left to timeout", timeout) + continue # incomplete message + + def __enter__(self): + self.connect() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() diff --git a/src/lib/nfc/handover/server.py b/src/lib/nfc/handover/server.py new file mode 100644 index 0000000..3817c34 --- /dev/null +++ b/src/lib/nfc/handover/server.py @@ -0,0 +1,128 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +# +# Negotiated Connection Handover - Server Base Class +# +import threading +import binascii +import logging +import errno +import ndef +import src.lib.nfc + + +log = logging.getLogger(__name__) + + +class HandoverServer(threading.Thread): + """ NFC Forum Connection Handover server + """ + def __init__(self, llc, request_size_limit=0x10000, + recv_miu=1984, recv_buf=15): + socket = nfc.llcp.Socket(llc, nfc.llcp.DATA_LINK_CONNECTION) + recv_miu = socket.setsockopt(nfc.llcp.SO_RCVMIU, recv_miu) + recv_buf = socket.setsockopt(nfc.llcp.SO_RCVBUF, recv_buf) + socket.bind('urn:nfc:sn:handover') + log.info("handover server bound to port {0} (MIU={1}, RW={2})" + .format(socket.getsockname(), recv_miu, recv_buf)) + socket.listen(backlog=2) + threading.Thread.__init__(self, name='urn:nfc:sn:handover', + target=self.listen, args=(llc, socket)) + + def listen(self, llc, socket): + log.debug("handover listen thread started") + try: + while True: + client_socket = socket.accept() + client_thread = threading.Thread(target=self.serve, + args=(client_socket,)) + client_thread.start() + except nfc.llcp.Error as error: + (log.debug if error.errno == errno.EPIPE else log.error)(error) + finally: + socket.close() + log.debug("handover listen thread terminated") + + def serve(self, socket): + peer_sap = socket.getpeername() + log.info("serving handover client on remote sap {0}".format(peer_sap)) + send_miu = socket.getsockopt(nfc.llcp.SO_SNDMIU) + try: + while socket.poll("recv"): + request = bytearray() + while socket.poll("recv"): + request += socket.recv() + + if len(request) == 0: + continue # need some data + + try: + list(ndef.message_decoder(request, 'strict', {})) + except ndef.DecodeError: + continue # need more data + + response = self._process_request_data(request) + + for offset in range(0, len(response), send_miu): + fragment = response[offset:offset + send_miu] + if not socket.send(fragment): + return # connection closed + + except nfc.llcp.Error as error: + (log.debug if error.errno == errno.EPIPE else log.error)(error) + finally: + socket.close() + log.debug("handover serve thread terminated") + + def _process_request_data(self, octets): + log.debug("<<< %s", binascii.hexlify(octets).decode()) + try: + records = list(ndef.message_decoder(octets, 'relax')) + except ndef.DecodeError as error: + log.error(repr(error)) + return b'' + + if records[0].type == 'urn:nfc:wkt:Hr': + records = self.process_handover_request_message(records) + else: + log.error("received unknown request message") + records = [] + + octets = b''.join(ndef.message_encoder(records)) + log.debug(">>> %s", binascii.hexlify(octets).decode()) + return octets + + def process_handover_request_message(self, records): + """Process a handover request message. The *records* argument holds a + list of :class:`ndef.Record` objects decoded from the received + handover request message octets, where the first record type is + ``urn:nfc:wkt:Hr``. The method returns a list of :class:`ndef.Record` + objects with the first record typ ``urn:nfc:wkt:Hs``. + + This method should be overwritten by a subclass to customize + it's behavior. The default implementation returns a + :class:`ndef.HandoverSelectRecord` with version ``1.2`` and no + alternative carriers. + + """ + log.warning("default process_request method should be overwritten") + return [ndef.HandoverSelectRecord('1.2')] diff --git a/src/lib/nfc/llcp/__init__.py b/src/lib/nfc/llcp/__init__.py new file mode 100644 index 0000000..74631ac --- /dev/null +++ b/src/lib/nfc/llcp/__init__.py @@ -0,0 +1,38 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +""" +The nfc.llcp module implements the NFC Forum Logical Link Control +Protocol (LLCP) specification and provides a socket interface to use +the connection-less and connection-mode transport facilities of LLCP. +""" +from .socket import Socket # noqa: F401 +from .llc import LOGICAL_DATA_LINK, DATA_LINK_CONNECTION # noqa: F401 +from .err import Error, ConnectRefused, errno # noqa: F401 + +SO_SNDMIU = 1 +SO_RCVMIU = 2 +SO_SNDBUF = 3 +SO_RCVBUF = 4 +SO_SNDBSY = 5 +SO_RCVBSY = 6 + +MSG_DONTWAIT = 0b00000001 diff --git a/src/lib/nfc/llcp/err.py b/src/lib/nfc/llcp/err.py new file mode 100644 index 0000000..1a0a498 --- /dev/null +++ b/src/lib/nfc/llcp/err.py @@ -0,0 +1,42 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import os +import errno + + +class Error(IOError): + def __init__(self, errno): + super(Error, self).__init__(errno, os.strerror(errno)) + + def __str__(self): + return "nfc.llcp.Error: [{0}] {1}".format( + errno.errorcode[self.errno], self.strerror) + + +class ConnectRefused(Error): + def __init__(self, reason): + super(ConnectRefused, self).__init__(errno.ECONNREFUSED) + self.reason = reason + + def __str__(self): + return "nfc.llcp.ConnectRefused: [{0}] {1} with reason {2}".format( + errno.errorcode[self.errno], self.strerror, self.reason) diff --git a/src/lib/nfc/llcp/llc.py b/src/lib/nfc/llcp/llc.py new file mode 100644 index 0000000..d92fcbc --- /dev/null +++ b/src/lib/nfc/llcp/llc.py @@ -0,0 +1,886 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +from . import tco +from . import pdu +from . import err +from . import sec +import src.lib.nfc.llcp +import src.lib.nfc.clf +import src.lib.nfc.dep + +import re +import time +import errno +import random +import threading +import collections + +import logging +log = logging.getLogger(__name__) + +RAW_ACCESS_POINT, LOGICAL_DATA_LINK, DATA_LINK_CONNECTION = range(3) + +wks_map = { + b"urn:nfc:sn:sdp": 1, + b"urn:nfc:sn:snep": 4, +} + +service_name_format = \ + re.compile(b"^urn:nfc:[x]?sn:[a-zA-Z][a-zA-Z0-9-_:\\.]*$") + + +class ServiceAccessPoint(object): + def __init__(self, addr, llc): + self.llc = llc + self.addr = addr + self.sock_list = collections.deque() + self.send_list = collections.deque() + + def __str__(self): + return "SAP {0:>2}".format(self.addr) + + @property + def mode(self): + with self.llc.lock: + try: + if isinstance(self.sock_list[0], tco.RawAccessPoint): + return RAW_ACCESS_POINT + if isinstance(self.sock_list[0], tco.LogicalDataLink): + return LOGICAL_DATA_LINK + if isinstance(self.sock_list[0], tco.DataLinkConnection): + return DATA_LINK_CONNECTION + except IndexError: + return 0 + + def insert_socket(self, socket): + with self.llc.lock: + try: + insertable = isinstance(socket, type(self.sock_list[0])) + except IndexError: + insertable = True + if insertable: + socket.bind(self.addr) + self.sock_list.appendleft(socket) + else: + log.error("can't insert socket of different type") + return insertable + + def remove_socket(self, socket): + assert socket.addr == self.addr + socket.close() + with self.llc.lock: + try: + self.sock_list.remove(socket) + except ValueError: + pass + if len(self.sock_list) == 0: + # completely remove this sap + self.llc.sap[self.addr] = None + + def send(self, send_pdu): + self.send_list.append(send_pdu) + + def shutdown(self): + while True: + try: + socket = self.sock_list.pop() + except IndexError: + return + log.debug("shutdown socket %s" % str(socket)) + socket.bind(None) + socket.close() + + # + # enqueue() and dequeue() are called from llc run thread + # + def enqueue(self, rcvd_pdu): + with self.llc.lock: + if isinstance(rcvd_pdu, pdu.Connect): + for socket in self.sock_list: + if socket.state.LISTEN: + socket.enqueue(rcvd_pdu) + break + else: + args = (rcvd_pdu.ssap, rcvd_pdu.dsap, 0x02) + self.send(pdu.DisconnectedMode(*args)) + else: + for socket in self.sock_list: + if rcvd_pdu.ssap == socket.peer or socket.peer is None: + socket.enqueue(rcvd_pdu) + break + else: + if rcvd_pdu.name in tco.DataLinkConnection.DLC_PDU_NAMES: + args = (rcvd_pdu.ssap, rcvd_pdu.dsap, 0x01) + self.send(pdu.DisconnectedMode(*args)) + else: + log.debug("%s discard PDU %s", self, rcvd_pdu) + + def dequeue(self, miu_size, icv_size): + with self.llc.lock: + for socket in self.sock_list: + send_pdu = socket.dequeue(miu_size, icv_size) + if send_pdu: + return send_pdu + else: + try: + return self.send_list.popleft() + except IndexError: + pass + + def sendack(self): + with self.llc.lock: + for socket in self.sock_list: + send_pdu = socket.sendack() + if send_pdu: + return send_pdu + + +class ServiceDiscovery(object): + def __init__(self, llc): + self.llc = llc + self.snl = dict() + self.tids = list(range(256)) + self.resp = threading.Condition(self.llc.lock) + self.sent = dict() + self.sdreq = collections.deque() + self.sdres = collections.deque() + self.dmpdu = collections.deque() + + def __str__(self): + return "SAP 1" + + @property + def mode(self): + return LOGICAL_DATA_LINK + + def resolve(self, name): + with self.resp: + if self.snl is None: + return None + log.debug("resolve service name %r", name) + try: + return self.snl[name] + except KeyError: + pass + tid = random.choice(self.tids) + self.tids.remove(tid) + self.sdreq.append((tid, name)) + while self.snl is not None and name not in self.snl: + self.resp.wait() + return None if self.snl is None else self.snl[name] + + # + # enqueue() and dequeue() are called from llc run thread + # + def enqueue(self, rcvd_pdu): + with self.llc.lock: + if ((isinstance(rcvd_pdu, pdu.ServiceNameLookup) + and self.snl is not None)): + + for tid, sap in rcvd_pdu.sdres: + try: + name = self.sent[tid] + except KeyError: + continue + log.debug("resolved %r to remote addr %d", name, sap) + csn, sap = sap >> 6 & 1, sap & 63 + if csn: + sap = 1 + self.snl[name] = sap + self.tids.append(tid) + self.resp.notify_all() + + for tid, name in rcvd_pdu.sdreq: + try: + sap = self.llc.snl[name] + except KeyError: + sap = 0 + self.sdres.append((tid, sap)) + + def dequeue(self, miu_size, icv_size): + with self.llc.lock: + if len(self.sdres) > 0 or len(self.sdreq) > 0: + send_pdu = pdu.ServiceNameLookup(dsap=1, ssap=1) + # add service discovery responses + while miu_size > 0: + try: + send_pdu.sdres.append(self.sdres.popleft()) + miu_size -= 4 + except IndexError: + break + # add service discovery requests + for i in range(len(self.sdreq)): + tid, name = self.sdreq[0] + if 3 + len(name) > miu_size: + self.sdreq.rotate(-1) + else: + send_pdu.sdreq.append(self.sdreq.popleft()) + self.sent[tid] = name + miu_size -= 3 + len(name) + return send_pdu + if len(self.dmpdu) > 0 and miu_size > 0: + return self.dmpdu.popleft() + + def shutdown(self): + with self.llc.lock: + self.snl = None + self.resp.notify_all() + + +class LogicalLinkController(object): + class LinkState(object): + def __init__(self): + self.names = ("SHUTDOWN", "LISTEN", "CONNECT", "CONNECTED", + "ESTABLISHED", "DISCONNECT", "CLOSED") + self.value = self.names.index("SHUTDOWN") + + def __str__(self): + return self.names[self.value] + + def __getattr__(self, name): + return self.value == self.names.index(name) + + def __setattr__(self, name, value): + if name not in ("names", "value"): + value, name = self.names.index(name), "value" + parent = super(LogicalLinkController.LinkState, self) + parent.__setattr__(name, value) + + class Counter(object): + def __init__(self): + self.sent = collections.defaultdict(int) + self.rcvd = collections.defaultdict(int) + + @property + def sent_count(self): + return sum(self.sent.values()) + + @property + def rcvd_count(self): + return sum(self.rcvd.values()) + + def __str__(self): + s = "sent/rcvd {0}/{1}".format(self.sent_count, self.rcvd_count) + for name in sorted(set(list(self.sent.keys()) + + list(self.rcvd.keys()))): + s += " {name} {sent}/{rcvd}".format( + name=name, sent=self.sent[name], rcvd=self.rcvd[name]) + return s + + def __init__(self, **options): + self.pcnt = LogicalLinkController.Counter() + self.link = LogicalLinkController.LinkState() + self.lock = threading.RLock() + self.cfg = dict() + self.cfg['recv-miu'] = options.get('miu', 248) + self.cfg['send-lto'] = options.get('lto', 500) + self.cfg['send-lsc'] = options.get('lsc', 3) + self.cfg['send-agf'] = options.get('agf', True) + self.cfg['llcp-sec'] = options.get('sec', True) + if not sec.OpenSSL: + self.cfg['llcp-sec'] = False + log.debug("llc cfg {0}".format(self.cfg)) + self.sec = None + self.snl = dict({b"urn:nfc:sn:sdp": 1}) + self.sap = 64 * [None] + self.sap[0] = ServiceAccessPoint(0, self) + self.sap[1] = ServiceDiscovery(self) + + def __str__(self): + local = "Local(MIU={miu}, LTO={lto}ms)".format( + miu=self.cfg.get('recv-miu'), lto=self.cfg.get('send-lto')) + remote = "Remote(MIU={miu}, LTO={lto}ms)".format( + miu=self.cfg.get('send-miu'), lto=self.cfg.get('recv-lto')) + return "LLC: {local} {remote}".format(local=local, remote=remote) + + @property + def secure_data_transfer(self): + return self.cfg.get('llcp-dpc', 0) == 1 + + def activate(self, mac, **options): + assert isinstance(mac, (nfc.dep.Initiator, nfc.dep.Target)) + self.mac = None + + wks = 1 + sum([1 << sap for sap in self.snl.values() if sap < 15]) + + send_pax = pdu.ParameterExchange() + send_pax.version = (1, 3) + send_pax.wks = wks + if self.cfg['recv-miu'] != 128: + send_pax.miu = self.cfg['recv-miu'] + if self.cfg['send-lto'] != 100: + send_pax.lto = self.cfg['send-lto'] + if self.cfg['send-lsc'] != 0: + send_pax.lsc = self.cfg['send-lsc'] + if self.cfg['llcp-sec']: + send_pax.dpc = 1 + + gb = b'Ffm' + pdu.encode(send_pax)[2:] + if isinstance(mac, nfc.dep.Initiator): + self.link.CONNECT = True + gb = mac.activate(gbi=gb, **options) + self.run = self.run_as_initiator + else: + self.link.LISTEN = True + gb = mac.activate(gbt=gb, **options) + self.run = self.run_as_target + + if gb and gb.startswith(b'Ffm') and len(gb) >= 6: + if ((isinstance(mac, nfc.dep.Target) + and mac.rwt >= send_pax.lto * 1E-3)): + msg = "local NFC-DEP RWT {0:.3f} contradicts LTO {1:.3f} sec" + log.warning(msg.format(mac.rwt, send_pax.lto*1E3)) + + rcvd_pax = pdu.decode(b"\x00\x40" + bytes(gb[3:])) + + log.debug("SENT {0}".format(send_pax)) + log.debug("RCVD {0}".format(rcvd_pax)) + + self.cfg['rcvd-ver'] = rcvd_pax.version + self.cfg['send-miu'] = rcvd_pax.miu + self.cfg['recv-lto'] = rcvd_pax.lto + self.cfg['send-wks'] = rcvd_pax.wks + self.cfg['send-lsc'] = rcvd_pax.lsc + self.cfg['llcp-dpc'] = rcvd_pax.dpc if self.cfg['llcp-sec'] else 0 + log.debug("llc cfg {0}".format(self.cfg)) + + info = '\n'.join([ + "LLCP Link established as NFC-DEP {role}", + "Local LLCP Settings", + " LLCP Version: {send_pax.version_text}", + " Link Timeout: {send_pax.lto} ms", + " Max Inf Unit: {send_pax.miu} octet", + " Link Service: {send_pax.lsc_text}", + " Data Protect: {send_pax.dpc_text}", + " Service List: {send_pax.wks:016b} ({send_pax.wks_text})", + "Remote LLCP Settings", + " LLCP Version: {rcvd_pax.version[0]}.{rcvd_pax.version[1]}", + " Link Timeout: {rcvd_pax.lto} ms", + " Max Inf Unit: {rcvd_pax.miu} octet", + " Link Service: {rcvd_pax.lsc_text}", + " Data Protect: {rcvd_pax.dpc_text}", + " Service List: {rcvd_pax.wks:016b} ({rcvd_pax.wks_text})" + ]).format(role=mac.role, send_pax=send_pax, rcvd_pax=rcvd_pax) + log.info(info) + + if isinstance(mac, nfc.dep.Initiator) and mac.rwt is not None: + max_rwt = 4096/13.56E6 * 2**10 + if mac.rwt > max_rwt: + msg = "remote NFC-DEP RWT {0:.3f} exceeds max {1:.3f} sec" + log.warning(msg.format(mac.rwt, max_rwt)) + + self.mac = mac + self.link.CONNECTED = True + + return bool(self.mac) + + def terminate(self, reason): + log.debug("llcp link termination caused by {0}".format(reason)) + if type(self.mac) == nfc.dep.Initiator: + if self.link.DISCONNECT is True: + self.exchange(pdu.Disconnect(0, 0), timeout=0.5) + self.mac.deactivate(release=False) # use DESELECT + if type(self.mac) == nfc.dep.Target: + self.mac.deactivate(data=bytearray(b"\x01\x40")) + # shutdown local services + for i in range(63, -1, -1): + if not self.sap[i] is None: + log.debug("closing service access point %d" % i) + self.sap[i].shutdown() + self.sap[i] = None + self.link.SHUTDOWN = True + + def exchange(self, send_pdu, timeout): + # Send and receive one protocol data unit. The send_pdu is + # None for the first call when running as target (because the + # target first receives a pdu). All PDUs except SYMM are + # logged with debug level, SYMM is logged with DEBUG-1 so that + # it must be explicitely enabled. The return value is either a + # PDU instance or None. + try: + if send_pdu: + loglevel = logging.DEBUG - bool(send_pdu.name == "SYMM") + log.log(loglevel, "SEND %s", send_pdu) + send_data = pdu.encode(send_pdu) + self.pcnt.sent[send_pdu.name] += 1 + rcvd_data = self.mac.exchange(send_data, timeout) + else: + rcvd_data = self.mac.exchange(None, timeout) + if rcvd_data is not None: + rcvd_pdu = pdu.decode(rcvd_data) + self.pcnt.rcvd[rcvd_pdu.name] += 1 + loglevel = logging.DEBUG - bool(rcvd_pdu.name == "SYMM") + log.log(loglevel, "RECV %s", rcvd_pdu) + return rcvd_pdu + except (nfc.clf.CommunicationError, pdu.Error) as error: + log.warning("{0!r}".format(error)) + + def run_as_initiator(self, terminate=lambda: False): + recv_timeout = 1E-3 * (self.cfg['recv-lto'] + 10) + msg = "starting initiator run loop with a receive timeout of %.3f sec" + log.debug(msg, recv_timeout) + + symm = 0 # counts the number of consecutive SYMM PDUs + try: + if self.cfg['llcp-dpc'] == 1: + cipher = sec.cipher_suite("ECDH_anon_WITH_AEAD_AES_128_CCM_4") + pubkey = cipher.public_key_x + cipher.public_key_y + random = cipher.random_nonce + send_dps = pdu.DataProtectionSetup(0, 0, pubkey, random) + rcvd_dps = self.exchange(send_dps, recv_timeout) + if not isinstance(rcvd_dps, pdu.DataProtectionSetup): + log.error("expected a DPS PDU response") + return self.terminate(reason="key agreement error") + if not (rcvd_dps.ecpk and len(rcvd_dps.ecpk) == 64): + log.error("absent or invalid ECPK parameter in DPS PDU") + return self.terminate(reason="key agreement error") + if not (rcvd_dps.rn and len(rcvd_dps.rn) == 8): + log.error("absent or invalid RN parameter in DPS PDU") + return self.terminate(reason="key agreement error") + cipher.calculate_session_key(rcvd_dps.ecpk, rn_t=rcvd_dps.rn) + self.sec = cipher + + send_pdu = self.collect(delay=0.01) + self.link.ESTABLISHED = True + while not terminate(): + if send_pdu is None: + send_pdu = pdu.Symmetry() + rcvd_pdu = self.exchange(send_pdu, recv_timeout) + if rcvd_pdu is None: + return self.terminate(reason="link disruption") + if rcvd_pdu == pdu.Disconnect(0, 0): + self.link.CLOSED = True + return self.terminate(reason="remote choice") + symm += 1 if rcvd_pdu.name == "SYMM" else 0 + self.dispatch(rcvd_pdu) + send_pdu = self.collect(delay=0.001) + if send_pdu is None and symm >= 10: + send_pdu = self.collect(delay=0.05) + else: + self.link.DISCONNECT = True + self.terminate(reason="local choice") + except KeyboardInterrupt: + print() # move to new line + self.link.DISCONNECT = True + self.terminate(reason="local choice") + raise KeyboardInterrupt + except IOError: + self.terminate(reason="input/output error") + raise SystemExit + except sec.KeyAgreementError: + self.terminate(reason="key agreement error") + raise SystemExit + except sec.DecryptionError: + self.terminate(reason="decryption error") + raise SystemExit + except sec.EncryptionError: + self.terminate(reason="encryption error") + raise SystemExit + finally: + log.debug("llc run loop terminated on initiator") + + def run_as_target(self, terminate=lambda: False): + recv_timeout = 1E-3 * (self.cfg['recv-lto'] + 10) + msg = "starting target run loop with a receive timeout of %.3f sec" + log.debug(msg, recv_timeout) + + symm = 0 # counts the number of consecutive SYMM PDUs + try: + if self.cfg['llcp-dpc'] == 1: + cipher = sec.cipher_suite("ECDH_anon_WITH_AEAD_AES_128_CCM_4") + pubkey = cipher.public_key_x + cipher.public_key_y + random = cipher.random_nonce + send_dps = pdu.DataProtectionSetup(0, 0, pubkey, random) + rcvd_dps = self.exchange(None, recv_timeout) + if not isinstance(rcvd_dps, pdu.DataProtectionSetup): + log.error("expected a DPS PDU request") + return self.terminate(reason="key agreement error") + if not (rcvd_dps.ecpk and len(rcvd_dps.ecpk) == 64): + log.error("absent or invalid ECPK parameter in DPS PDU") + return self.terminate(reason="key agreement error") + if not (rcvd_dps.rn and len(rcvd_dps.rn) == 8): + log.error("absent or invalid RN parameter in DPS PDU") + return self.terminate(reason="key agreement error") + rcvd_pdu = self.exchange(send_dps, recv_timeout) + cipher.calculate_session_key(rcvd_dps.ecpk, rn_i=rcvd_dps.rn) + self.sec = cipher + else: + rcvd_pdu = self.exchange(None, recv_timeout) + + self.link.ESTABLISHED = True + while not terminate(): + if rcvd_pdu is None: + return self.terminate(reason="link disruption") + if rcvd_pdu == pdu.Disconnect(0, 0): + self.link.CLOSED = True + return self.terminate(reason="remote choice") + symm += 1 if isinstance(rcvd_pdu, pdu.Symmetry) else 0 + self.dispatch(rcvd_pdu) + send_pdu = self.collect(delay=0.001) + if send_pdu is None and symm >= 10: + send_pdu = self.collect(delay=0.05) + if send_pdu is None: + send_pdu = pdu.Symmetry() + rcvd_pdu = self.exchange(send_pdu, recv_timeout) + else: + self.link.DISCONNECT = True + self.terminate(reason="local choice") + except KeyboardInterrupt: + print() # move to new line + self.link.DISCONNECT = True + self.terminate(reason="local choice") + raise KeyboardInterrupt + except IOError: + self.terminate(reason="input/output error") + raise SystemExit + except sec.KeyAgreementError: + self.terminate(reason="key agreement error") + raise SystemExit + except sec.DecryptionError: + self.terminate(reason="decryption error") + raise SystemExit + except sec.EncryptionError: + self.terminate(reason="encryption error") + raise SystemExit + finally: + log.debug("llc run loop terminated on target") + + def collect(self, delay=None): + # Collect a single PDU or multiple PDUs if aggregation is enabled. + if delay: + time.sleep(delay) + + def encrypt(send_pdu): + pdu_type = type(send_pdu) + a = send_pdu.encode_header() + c = self.sec.encrypt(a, send_pdu.data) + return pdu_type(*pdu_type.decode_header(a), data=c) + + miu_size = self.cfg["send-miu"] + icv_size = self.sec.icv_size if self.sec else 0 + send_pdu = None + + with self.lock: + # Dequeue from the list of active SAP until a first PDU is + # returned. The list is sorted to first iterate the raw + # SAPs (raw SAPs do not respect the miu_size value and we + # must avoid them to return PDUs in aggregation). The PDU + # is returned straight if it fills or exceeds the Link + # MIU. Otherwise the loop terminates at this point. The + # sap.dequeue method is called with icv_size=0 because for + # encrypted but not aggregated UI and I PDUs the receiver + # must accept them with complete MIU plus ICV size. + for sap in sorted(filter(None, self.sap), reverse=True, + key=lambda sap: sap.mode == RAW_ACCESS_POINT): + send_pdu = sap.dequeue(miu_size, icv_size=0) + if send_pdu: + if self.sec and send_pdu.name in ("UI", "I"): + send_pdu = encrypt(send_pdu) + if len(send_pdu) - send_pdu.header_size >= miu_size: + return send_pdu + break + + # Data Link Connection endpoints do not dequeue RR/RNR PDUs until + # the receive window is exhausted. If there is not yet a PDU to + # send, this loop allows voluntary acknowledgement. + if send_pdu is None: + for sap in filter(None, self.sap): + if sap.mode == DATA_LINK_CONNECTION: + send_pdu = sap.sendack() + if send_pdu: + break + + # Finish if either there is either no PDU to send or if PDU + # aggregation is disabled. + if send_pdu is None or self.cfg['send-agf'] is False: + return send_pdu + + # We have one PDU to send and aggregation is enabled. We'll see if + # there are more outbound PDUs and collect them into an AGF PDU. + agf_pdu = pdu.AggregatedFrame(0, 0, [send_pdu]) + miu_size = self.cfg["send-miu"] - len(agf_pdu) - 3 + while True: + # The first loop will dequeue PDUs until the reamining miu_size + # is exhausted or all active SAP did not return a PDU. + deq_none = True + for sap in filter(None, self.sap): + send_pdu = sap.dequeue(miu_size, icv_size) + if send_pdu: + deq_none = False + if self.sec and send_pdu.name in ("UI", "I"): + send_pdu = encrypt(send_pdu) + agf_pdu.append(send_pdu) + miu_size = self.cfg["send-miu"] - len(agf_pdu) - 3 + if miu_size < 0: + break + if miu_size < 0 or deq_none: + break + # If the miu_size is not yet exhausted we query all data link + # connection endpoints once for voluntary acknowledgements. + if miu_size >= 0: + for sap in filter(None, self.sap): + if sap.mode == DATA_LINK_CONNECTION: + send_pdu = sap.sendack() + if send_pdu: + agf_pdu.append(send_pdu) + miu_size = self.cfg["send-miu"] - len(agf_pdu) - 3 + if miu_size < 0: + break + + return agf_pdu if agf_pdu.count > 1 else agf_pdu.first + + def dispatch(self, rcvd_pdu): + if rcvd_pdu is None or rcvd_pdu.name == "SYMM": + return + + if rcvd_pdu.name == "AGF": + if rcvd_pdu.dsap == 0 and rcvd_pdu.ssap == 0: + for p in rcvd_pdu: + log.debug(" " + str(p)) + for p in rcvd_pdu: + self.dispatch(p) + return + + if rcvd_pdu.name == "CONNECT" and rcvd_pdu.dsap == 1: + # connect-by-name + addr = self.snl.get(rcvd_pdu.sn) + if not addr or self.sap[addr] is None: + dm_reason = 0x10 if rcvd_pdu.sn is None else 0x02 + dm_pdu = pdu.DisconnectedMode(rcvd_pdu.ssap, 1, dm_reason) + self.sap[1].dmpdu.append(dm_pdu) + log.debug("could not find service %r", rcvd_pdu.sn) + return + # service found, rewrite CONNECT PDU to its DSAP + rcvd_pdu = pdu.Connect(dsap=addr, ssap=rcvd_pdu.ssap, + rw=rcvd_pdu.rw, miu=rcvd_pdu.miu) + + if self.sec and rcvd_pdu.name in ("UI", "I"): + pdu_type = type(rcvd_pdu) + a = rcvd_pdu.encode_header() + p = self.sec.decrypt(a, rcvd_pdu.data) + rcvd_pdu = pdu_type(*pdu_type.decode_header(a), data=p) + + with self.lock: + sap = self.sap[rcvd_pdu.dsap] + if sap: + sap.enqueue(rcvd_pdu) + else: + log.debug("can't dispatch PDU %s", rcvd_pdu) + + def resolve(self, name): + if isinstance(name, (bytes, bytearray)): + return self.sap[1].resolve(bytes(name)) + return self.sap[1].resolve(name.encode('latin')) + + def socket(self, socket_type): + if socket_type == RAW_ACCESS_POINT: + return tco.RawAccessPoint(recv_miu=self.cfg["recv-miu"]) + if socket_type == LOGICAL_DATA_LINK: + return tco.LogicalDataLink(recv_miu=self.cfg["recv-miu"]) + if socket_type == DATA_LINK_CONNECTION: + return tco.DataLinkConnection(recv_miu=128, recv_win=1) + + def setsockopt(self, socket, option, value): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if option == nfc.llcp.SO_RCVMIU: + value = min(value, self.cfg['recv-miu']) + socket.setsockopt(option, value) + return socket.getsockopt(option) + + def getsockopt(self, socket, option): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if isinstance(socket, tco.LogicalDataLink): + # FIXME: set socket send miu when activated + socket.send_miu = self.cfg['send-miu'] + if isinstance(socket, tco.RawAccessPoint): + # FIXME: set socket send miu when activated + socket.send_miu = self.cfg['send-miu'] + return socket.getsockopt(option) + + def bind(self, socket, addr_or_name=None): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if socket.addr is not None: + raise err.Error(errno.EINVAL) + if addr_or_name is None: + self._bind_by_none(socket) + elif isinstance(addr_or_name, int): + self._bind_by_addr(socket, addr_or_name) + elif isinstance(addr_or_name, (bytes, bytearray)): + self._bind_by_name(socket, bytes(addr_or_name)) + elif isinstance(addr_or_name, str): + self._bind_by_name(socket, addr_or_name.encode('latin')) + else: + raise err.Error(errno.EFAULT) + + def _bind_by_none(self, socket): + with self.lock: + try: + addr = 32 + self.sap[32:64].index(None) + except ValueError: + raise err.Error(errno.EAGAIN) + else: + socket.bind(addr) + self.sap[addr] = ServiceAccessPoint(addr, self) + self.sap[addr].insert_socket(socket) + + def _bind_by_addr(self, socket, addr): + if addr < 0 or addr > 63: + raise err.Error(errno.EFAULT) + with self.lock: + if addr in range(32, 64) or isinstance(socket, tco.RawAccessPoint): + if self.sap[addr] is None: + socket.bind(addr) + self.sap[addr] = ServiceAccessPoint(addr, self) + self.sap[addr].insert_socket(socket) + else: + raise err.Error(errno.EADDRINUSE) + else: + raise err.Error(errno.EACCES) + + def _bind_by_name(self, socket, name): + if not service_name_format.match(name): + raise err.Error(errno.EFAULT) + + with self.lock: + if self.snl.get(name) is not None: + raise err.Error(errno.EADDRINUSE) + addr = wks_map.get(name) + if addr is None: + try: + addr = 16 + self.sap[16:32].index(None) + except ValueError: + raise err.Error(errno.EADDRNOTAVAIL) + socket.bind(addr) + self.sap[addr] = ServiceAccessPoint(addr, self) + self.sap[addr].insert_socket(socket) + self.snl[name] = addr + + def connect(self, socket, dest): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if not socket.is_bound: + self.bind(socket) + socket.connect(dest) + log.debug("connected ({0} ===> {1})".format(socket.addr, socket.peer)) + if socket.send_miu > self.cfg['send-miu']: + log.warning("reducing outbound miu to not exceed the link miu") + socket.send_miu = self.cfg['send-miu'] + + def listen(self, socket, backlog): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if not isinstance(socket, tco.DataLinkConnection): + raise err.Error(errno.EOPNOTSUPP) + if not isinstance(backlog, int): + raise TypeError("backlog must be int type") + if backlog < 0: + raise ValueError("backlog can not be negative") + backlog = min(backlog, 16) + if not socket.is_bound: + self.bind(socket) + socket.listen(backlog) + + def accept(self, socket): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if not isinstance(socket, tco.DataLinkConnection): + raise err.Error(errno.EOPNOTSUPP) + while True: + client = socket.accept() + self.sap[client.addr].insert_socket(client) + log.debug("new data link connection ({0} <=== {1})" + .format(client.addr, client.peer)) + if client.send_miu > self.cfg['send-miu']: + log.warning("reducing outbound miu to comply with link miu") + client.send_miu = self.cfg['send-miu'] + return client + + def send(self, socket, message, flags): + return self.sendto(socket, message, socket.peer, flags) + + def sendto(self, socket, message, dest, flags): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if isinstance(socket, tco.RawAccessPoint): + if not isinstance(message, pdu.ProtocolDataUnit): + raise TypeError("on a raw access point message must be a pdu") + if not socket.is_bound: + self.bind(socket) + # FIXME: set socket send miu when activated + socket.send_miu = self.cfg['send-miu'] + return socket.send(message, flags) + if not isinstance(message, (bytes, bytearray)): + raise TypeError("message data must be a bytes-like object") + if isinstance(socket, tco.LogicalDataLink): + if dest is None: + raise err.Error(errno.EDESTADDRREQ) + if not socket.is_bound: + self.bind(socket) + # FIXME: set socket send miu when activated + socket.send_miu = self.cfg['send-miu'] + return socket.sendto(message, dest, flags) + if isinstance(socket, tco.DataLinkConnection): + return socket.send(message, flags) + + def recv(self, socket): + message, sender = self.recvfrom(socket) + return message + + def recvfrom(self, socket): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if not (socket.addr and self.sap[socket.addr]): + raise err.Error(errno.EBADF) + if isinstance(socket, tco.RawAccessPoint): + return (socket.recv(), None) + if isinstance(socket, tco.LogicalDataLink): + return socket.recvfrom() + if isinstance(socket, tco.DataLinkConnection): + return (socket.recv(), socket.peer) + + def poll(self, socket, event, timeout=None): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if not (socket.addr and self.sap[socket.addr]): + raise err.Error(errno.EBADF) + return socket.poll(event, timeout) + + def close(self, socket): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + if socket.is_bound: + self.sap[socket.addr].remove_socket(socket) + else: + socket.close() + + def getsockname(self, socket): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + return socket.addr + + def getpeername(self, socket): + if not isinstance(socket, tco.TransmissionControlObject): + raise err.Error(errno.ENOTSOCK) + return socket.peer diff --git a/src/lib/nfc/llcp/pdu.py b/src/lib/nfc/llcp/pdu.py new file mode 100644 index 0000000..1ca160e --- /dev/null +++ b/src/lib/nfc/llcp/pdu.py @@ -0,0 +1,945 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import struct +from binascii import hexlify + +import logging +log = logging.getLogger(__name__) + + +class Error(Exception): + pass + + +class DecodeError(Error): + pass + + +class EncodeError(Error): + pass + + +class Parameter: + VERSION, MIUX, WKS, LTO, RW, SN, OPT, SDREQ, SDRES, ECPK, RN = range(1, 12) + + @staticmethod + def decode(data, offset): + try: + T, L = struct.unpack_from('BB', data, offset) + V = struct.unpack_from('%ds' % L, data, offset+2)[0] + except struct.error as error: + msg = " while decoding TLV %r" % hexlify(data[offset:]) + raise DecodeError(str(error) + msg) + + if T == Parameter.VERSION: + if L != 1: + raise DecodeError("VERSION TLV length error") + V = struct.unpack('B', V)[0] + elif T == Parameter.MIUX: + if L != 2: + raise DecodeError("MIUX TLV length error") + V = struct.unpack('>H', V)[0] + if V & 0xF800: + log.warning("MIUX TLV reserved bits set") + V = V & 0x07FF + elif T == Parameter.WKS: + if L != 2: + raise DecodeError("WKS TLV length error") + V = struct.unpack('>H', V)[0] + elif T == Parameter.LTO: + if L != 1: + raise DecodeError("LTO TLV length error") + V = struct.unpack('B', V)[0] + elif T == Parameter.RW: + if L != 1: + raise DecodeError("RW TLV length error") + V = struct.unpack('B', V)[0] + if V & 0xF0: + log.warning("RW TLV reserved bits set") + V = V & 0x0F + elif T == Parameter.SN and L == 0: + log.warning("SN TLV with zero-length service name") + elif T == Parameter.OPT: + if L != 1: + raise DecodeError("OPT TLV length error") + V = struct.unpack_from('B', V)[0] + if V & 0xF8: + log.warning("OPT TLV reserved bits set") + V = V & 0x07 + elif T == Parameter.SDREQ: + if L == 0: + raise DecodeError("SDREQ TLV length error") + if L == 1: + log.warning("SDREQ TLV with zero-length service name") + V = struct.unpack('B%ds' % (L-1), V) + elif T == Parameter.SDRES: + if L != 2: + raise DecodeError("SDRES TLV length error") + V = struct.unpack('BB', V) + elif T == Parameter.ECPK: + if L == 0: + log.warning("ECPK TLV with zero-length value") + if L & 1: + log.warning("ECPK TLV with odd length value") + elif T == Parameter.RN: + if L == 0: + log.warning("RN TLV with zero-length value") + + return (T, L, V) + + @staticmethod + def encode(T, V): + try: + if T in (Parameter.VERSION, Parameter.LTO, + Parameter.RW, Parameter.OPT): + return struct.pack('BBB', T, 1, V) + if T in (Parameter.MIUX, Parameter.WKS): + return struct.pack('>BBH', T, 2, V) + if T in (Parameter.SN, Parameter.ECPK, Parameter.RN): + if len(V) > 255: + raise EncodeError("can't encode TLV T=%d, V=%r" % (T, V)) + return struct.pack('BB', T, len(V)) + bytes(V) + if T == Parameter.SDREQ: + tid, sn = V[0], V[1] + if len(sn) > 254: + raise EncodeError("can't encode TLV T=%d, V=%r" % (T, V)) + return struct.pack('>BBB', T, 1+len(sn), tid) + bytes(sn) + if T == Parameter.SDRES: + tid, sap = V[0], V[1] + return struct.pack('>BBBB', T, 2, tid, sap) + raise EncodeError("unknown TLV T=%d, V=%r" % (T, V)) + except struct.error as error: + msg = " for TLV T=%d, V=%r" % (T, V) + raise EncodeError(str(error) + msg) + + +# ----------------------------------------------------------------------------- +# ProtocolDataUnit Base Class +# ----------------------------------------------------------------------------- +class ProtocolDataUnit(object): + header_size = 2 + + def __init__(self, ptype, dsap, ssap): + self.ptype = ptype + self.dsap = dsap + self.ssap = ssap + + @classmethod + def decode_header(cls, data, offset=0, size=None): + if size is None: + size = len(data) - offset + if size < cls.header_size: + raise DecodeError("insufficient pdu header bytes") + (dsap, ssap) = struct.unpack_from('!BB', data, offset) + return (dsap >> 2, ssap & 63) + + def encode_header(self): + if self.dsap is None or self.ssap is None: + raise EncodeError("pdu dsap and ssap field can not be None") + if self.dsap < 0 or self.ssap < 0: + raise EncodeError("pdu dsap and ssap field can not be < 0") + if self.dsap > 63 or self.ssap > 63: + raise EncodeError("pdu dsap and ssap field can not be > 63") + return struct.pack('!H', self.dsap << 10 | self.ptype << 6 | self.ssap) + + def __eq__(self, other): + return self.encode() == other.encode() + + def __str__(self): + string = "{pdu.ssap:2} -> {pdu.dsap:2} {pdu.name:4.4s}" + return string.format(pdu=self) + + +# ----------------------------------------------------------------------------- +# NumberedProtocolDataUnit Base Class +# ----------------------------------------------------------------------------- +class NumberedProtocolDataUnit(ProtocolDataUnit): + header_size = 3 + + def __init__(self, ptype, dsap, ssap, ns, nr): + super(NumberedProtocolDataUnit, self).__init__(ptype, dsap, ssap) + self.ns, self.nr = ns, nr + + @classmethod + def decode_header(cls, data, offset=0, size=None): + if size is None: + size = len(data) - offset + if size < cls.header_size: + raise DecodeError("numbered pdu header length error") + (dsap, ssap, sequence) = struct.unpack_from('!BBB', data, offset) + return (dsap >> 2, ssap & 63, sequence >> 4, sequence & 15) + + def encode_header(self): + data = super(NumberedProtocolDataUnit, self).encode_header() + if self.ns is None or self.nr is None: + raise EncodeError("pdu ns and nr field can not be None") + if self.ns < 0 or self.nr < 0: + raise EncodeError("pdu ns and nr field can not be < 0") + if self.ns > 15 or self.nr > 15: + raise EncodeError("pdu ns and nr field can not be > 15") + return data + struct.pack('!B', self.ns << 4 | self.nr) + + def __len__(self): + return 3 + + def __str__(self): + f = " N(R)={p.nr}" if self.ns is None else " N(S)={p.ns} N(R)={p.nr}" + return super(NumberedProtocolDataUnit, self).__str__()+f.format(p=self) + + +# ----------------------------------------------------------------------------- +# Symmetry PDU +# ----------------------------------------------------------------------------- +class Symmetry(ProtocolDataUnit): + name = "SYMM" + + def __init__(self, dsap=0, ssap=0): + super(Symmetry, self).__init__(0b0000, dsap, ssap) + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + if dsap != 0 or ssap != 0: + raise DecodeError("SSAP and DSAP must be 0 in SYMM PDU") + if size >= 3: + raise DecodeError("SYMM PDU PAYLOAD must be empty") + return Symmetry(dsap, ssap) + + def encode(self): + if self.dsap != 0 or self.ssap != 0: + raise EncodeError("SSAP and DSAP must be 0 in SYMM PDU") + return self.encode_header() + + def __len__(self): + return 2 + + def __str__(self): + return super(Symmetry, self).__str__() + + +# ----------------------------------------------------------------------------- +# Parameter Exchange PDU +# ----------------------------------------------------------------------------- +class ParameterExchange(ProtocolDataUnit): + name = "PAX" + + def __init__(self, dsap=0, ssap=0, version=None, miux=None, + wks=None, lto=None, opt=None): + super(ParameterExchange, self).__init__(0b0001, dsap, ssap) + self._version = version + self._miux = miux + self._wks = wks + self._lto = lto + self._opt = opt + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + if dsap != 0 or ssap != 0: + raise DecodeError("SSAP and DSAP must be 0 in PAX PDU") + pax_pdu = ParameterExchange(dsap, ssap) + offset, size = offset + 2, size - 2 + while size >= 2: + T, L, V = Parameter.decode(data, offset) + if T == Parameter.VERSION: + pax_pdu._version = V + elif T == Parameter.MIUX: + pax_pdu._miux = V + elif T == Parameter.WKS: + pax_pdu._wks = V + elif T == Parameter.LTO: + pax_pdu._lto = V + elif T == Parameter.OPT: + pax_pdu._opt = V + else: + log.warning("invalid TLV %r in PAX PDU", (T, L, V)) + offset, size = offset + 2 + L, size - 2 - L + return pax_pdu + + def encode(self): + if self.dsap != 0 or self.ssap != 0: + raise EncodeError("SSAP and DSAP must be 0 in PAX PDU") + data = self.encode_header() + if self._version is not None: + data += Parameter.encode(Parameter.VERSION, self._version) + if self._miux is not None: + data += Parameter.encode(Parameter.MIUX, self._miux) + if self._wks is not None: + data += Parameter.encode(Parameter.WKS, self._wks) + if self._lto is not None: + data += Parameter.encode(Parameter.LTO, self._lto) + if self._opt is not None: + data += Parameter.encode(Parameter.OPT, self._opt) + return data + + def __len__(self): + return (2 + + (3 if self._version is not None else 0) + + (4 if self._miux is not None else 0) + + (4 if self._wks is not None else 0) + + (3 if self._lto is not None else 0) + + (3 if self._opt is not None else 0)) + + @property + def version(self): + version = self._version + return (version >> 4, version & 15) if version else (0, 0) + + @version.setter + def version(self, value): + self._version = (value[0] << 4 & 0xF0) | (value[1] & 0x0F) + + @property + def version_text(self): + return "{0}.{1}".format(*self.version) + + @property + def miu(self): + return self._miux + 128 if self._miux is not None else 128 + + @miu.setter + def miu(self, value): + self._miux = max(value - 128, 0) + + @property + def wks(self): + return self._wks if self._wks is not None else 0 + + @wks.setter + def wks(self, value): + self._wks = value & 0xFFFF + + @property + def wks_text(self): + t = {0: "LLC", 1: "SDP", 4: "SNEP"} + return ', '.join([ + t.get(i, str(i)) for i in range(15, -1, -1) if self.wks >> i & 1]) + + @property + def lto(self): + return (self._lto if self._lto is not None else 10) * 10 + + @lto.setter + def lto(self, value): + self._lto = (value // 10) & 0xFF + + @property + def lsc(self): + return self._opt & 3 if self._opt is not None else 0 + + @lsc.setter + def lsc(self, value): + self._opt = ((self._opt or 0) & 0b11111100) | (value & 0b00000011) + + @property + def lsc_text(self): + return ("link service class unknown at activation", + "connection-less link service only", + "connection-oriented link service only", + "connection-less and connection-oriented")[self.lsc] + + @property + def dpc(self): + return self._opt >> 2 & 1 if self._opt is not None else 0 + + @dpc.setter + def dpc(self, value): + self._opt = ((self._opt or 0) & 0b11111011) | (bool(value) << 2) + + @property + def dpc_text(self): + return ("secure data transfer mode not supported", + "secure data transfer mode is supported")[self.dpc] + + def __str__(self): + s = super(ParameterExchange, self).__str__() + if self._version is not None: + s += " VER={0}.{1}".format(*self.version) + if self._wks is not None: + s += " WKS={0:016b}".format(self._wks) + if self._miux is not None: + s += " MIUX={0}".format(self._miux) + if self._lto is not None: + s += " LTO={0}".format(self._lto) + if self._opt is not None: + s += " OPT={0:08b}".format(self._opt) + return s + + +# ----------------------------------------------------------------------------- +# Aggregated Frame PDU +# ----------------------------------------------------------------------------- +class AggregatedFrame(ProtocolDataUnit): + name = "AGF" + + def __init__(self, dsap=0, ssap=0, aggregate=[]): + super(AggregatedFrame, self).__init__(0b0010, dsap, ssap) + self._aggregate = aggregate[:] + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + if dsap != 0 or ssap != 0: + raise DecodeError("SSAP and DSAP must be 0 in AGF PDU") + agf_pdu = AggregatedFrame(dsap, ssap) + offset, size = offset + 2, size - 2 + while size > 0: + try: + (pdu_size,) = struct.unpack_from('!H', data, offset) + except struct.error: + raise DecodeError("aggregated PDU length field error in AGF") + agf_pdu.append(decode(data, offset+2, pdu_size)) + offset, size = offset + 2 + pdu_size, size - 2 - pdu_size + return agf_pdu + + def encode(self): + if self.dsap != 0 or self.ssap != 0: + raise EncodeError("SSAP and DSAP must be 0 in AGF PDU") + data = self.encode_header() + for encoded_pdu in [pdu.encode() for pdu in self._aggregate]: + data += struct.pack('!H', len(encoded_pdu)) + encoded_pdu + return data + + def append(self, pdu): + self._aggregate.append(pdu) + + @property + def count(self): + return len(self._aggregate) + + @property + def first(self): + return self._aggregate[0] + + def __len__(self): + return 2 + sum([2+len(pdu) for pdu in self._aggregate]) + + def __str__(self): + def s(p): + return "LEN={0} '".format(len(p)) + \ + ProtocolDataUnit.__str__(p).rstrip() + "'" + return super(AggregatedFrame, self).__str__() + \ + " LEN={0} [".format(len(self)-2) + \ + " ".join([s(p) for p in self._aggregate]) + "]" + + def __iter__(self): + return AggregatedFrameIterator(self._aggregate) + + +class AggregatedFrameIterator(object): + def __init__(self, aggregate): + self._aggregate = aggregate + self._current = 0 + + def __iter__(self): + return self + + def __next__(self): + if self._current == len(self._aggregate): + raise StopIteration + self._current += 1 + return self._aggregate[self._current-1] + + def next(self): + return self.__next__() + + +# ----------------------------------------------------------------------------- +# Unnumbered Information PDU +# ----------------------------------------------------------------------------- +class UnnumberedInformation(ProtocolDataUnit): + name = "UI" + + def __init__(self, dsap, ssap, data=None): + super(UnnumberedInformation, self).__init__(0b0011, dsap, ssap) + self.data = data if data else b'' + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + payload = bytes(data[offset+2:offset+size]) + return UnnumberedInformation(dsap, ssap, payload) + + def encode(self): + return self.encode_header() + bytes(self.data) + + def __len__(self): + return 2 + len(self.data) + + def __str__(self): + return super(UnnumberedInformation, self).__str__() + \ + " LEN={0} DATA={1}".format(len(self.data), hexlify(self.data)) + + +# ----------------------------------------------------------------------------- +# Connect PDU +# ----------------------------------------------------------------------------- +class Connect(ProtocolDataUnit): + name = "CONNECT" + + def __init__(self, dsap, ssap, miu=128, rw=1, sn=None): + super(Connect, self).__init__(0b0100, dsap, ssap) + self.miu = miu + self.rw = rw + self.sn = sn + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + connect_pdu = Connect(dsap, ssap) + offset, size = offset + 2, size - 2 + while size >= 2: + T, L, V = Parameter.decode(data, offset) + if T == Parameter.MIUX: + connect_pdu.miu = 128 + V + elif T == Parameter.RW: + connect_pdu.rw = V + elif T == Parameter.SN: + connect_pdu.sn = V + else: + log.warning("invalid TLV %r in CONNECT PDU", (T, L, V)) + offset, size = offset + 2 + L, size - 2 - L + return connect_pdu + + def encode(self): + data = self.encode_header() + if self.miu and self.miu > 128: + data += Parameter.encode(Parameter.MIUX, self.miu - 128) + if self.rw and self.rw != 1: + data += Parameter.encode(Parameter.RW, self.rw) + if self.sn: + data += Parameter.encode(Parameter.SN, self.sn) + return data + + def __len__(self): + return (2 + + (4 if self.miu and self.miu > 128 else 0) + + (3 if self.rw and self.rw != 1 else 0) + + (2 + len(self.sn) if self.sn else 0)) + + def __str__(self): + s = " MIU={conn.miu} RW={conn.rw} SN={conn.sn}" + return super(Connect, self).__str__() + s.format(conn=self) + + +# ----------------------------------------------------------------------------- +# Disconnect PDU +# ----------------------------------------------------------------------------- +class Disconnect(ProtocolDataUnit): + name = "DISC" + + def __init__(self, dsap, ssap): + super(Disconnect, self).__init__(0b0101, dsap, ssap) + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + return Disconnect(dsap, ssap) + + def encode(self): + return self.encode_header() + + def __len__(self): + return 2 + + def __str__(self): + return super(Disconnect, self).__str__() + + +# ----------------------------------------------------------------------------- +# Connection Complete PDU +# ----------------------------------------------------------------------------- +class ConnectionComplete(ProtocolDataUnit): + name = "CC" + + def __init__(self, dsap, ssap, miu=128, rw=1): + super(ConnectionComplete, self).__init__(0b0110, dsap, ssap) + self.miu = miu + self.rw = rw + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + cc_pdu = ConnectionComplete(dsap, ssap) + offset, size = offset + 2, size - 2 + while size >= 2: + T, L, V = Parameter.decode(data, offset) + if T == Parameter.MIUX: + cc_pdu.miu = 128 + V + elif T == Parameter.RW: + cc_pdu.rw = V + else: + log.warning("invalid TLV %r in CC PDU", (T, L, V)) + offset, size = offset + 2 + L, size - 2 - L + return cc_pdu + + def encode(self): + data = self.encode_header() + if self.miu and self.miu > 128: + data += Parameter.encode(Parameter.MIUX, self.miu - 128) + if self.rw and self.rw != 1: + data += Parameter.encode(Parameter.RW, self.rw) + return data + + def __len__(self): + return (2 + + (4 if self.miu and self.miu > 128 else 0) + + (3 if self.rw and self.rw != 1 else 0)) + + def __str__(self): + return super(ConnectionComplete, self).__str__() + \ + " MIU={cc.miu} RW={cc.rw}".format(cc=self) + + +# ----------------------------------------------------------------------------- +# Disconnected Mode PDU +# ----------------------------------------------------------------------------- +class DisconnectedMode(ProtocolDataUnit): + name = "DM" + + def __init__(self, dsap, ssap, reason=0): + super(DisconnectedMode, self).__init__(0b0111, dsap, ssap) + self.reason = reason + + @classmethod + def decode(cls, data, offset, size): + if size != 3: + raise DecodeError("DM PDU length error") + dsap, ssap = cls.decode_header(data, offset, size) + (reason,) = struct.unpack_from('!B', data, offset+2) + return DisconnectedMode(dsap, ssap, reason) + + def encode(self): + return self.encode_header() + struct.pack('!B', self.reason) + + def __len__(self): + return 3 + + def __str__(self): + return super(DisconnectedMode, self).__str__() + \ + " REASON={dm.reason:02x}h".format(dm=self) + + @property + def reason_text(self): + return { + 0x00: "disconnected", + 0x01: "inactive", + 0x02: "unbound", + 0x03: "rejected", + 0x10: "permanent reject for sap", + 0x11: "permanent reject for any", + 0x20: "temporary reject for sap", + 0x21: "temporary reject for any", + }.get(self.reason, "{0:02x}h".format(self.reason)) + + +# ----------------------------------------------------------------------------- +# Frame Reject PDU +# ----------------------------------------------------------------------------- +class FrameReject(ProtocolDataUnit): + name = "FRMR" + + def __init__(self, dsap, ssap, flags=0, ptype=0, + ns=0, nr=0, vs=0, vr=0, vsa=0, vra=0): + super(FrameReject, self).__init__(0b1000, dsap, ssap) + self.rej_flags = flags + self.rej_ptype = ptype + self.ns = ns + self.nr = nr + self.vs = vs + self.vr = vr + self.vsa = vsa + self.vra = vra + + @classmethod + def decode(cls, data, offset, size): + if size != 6: + raise DecodeError("FRMR PDU length error") + dsap, ssap = cls.decode_header(data, offset, size) + (b0, b1, b2, b3) = struct.unpack_from('!BBBB', data, offset+2) + flags, ptype = b0 >> 4, b0 & 15 + ns, nr = b1 >> 4, b1 & 15 + vs, vr = b2 >> 4, b2 & 15 + vsa, vra = b3 >> 4, b3 & 15 + return FrameReject(dsap, ssap, flags, ptype, ns, nr, vs, vr, vsa, vra) + + @staticmethod + def from_pdu(pdu, flags, dlc): + rej_ptype = pdu.ptype + rej_flags = sum([1 << "SRIW".index(f) for f in flags]) + frmr = FrameReject(pdu.ssap, pdu.dsap, rej_flags, rej_ptype) + if isinstance(pdu, Information): + frmr.ns, frmr.nr = pdu.ns, pdu.nr + if isinstance(pdu, ReceiveReady) or isinstance(pdu, ReceiveNotReady): + frmr.nr = pdu.nr + frmr.vs, frmr.vsa = dlc.send_cnt, dlc.send_ack + frmr.vr, frmr.vra = dlc.recv_cnt, dlc.recv_ack + return frmr + + def encode(self): + return self.encode_header() + struct.pack( + '!BBBB', self.rej_flags << 4 | self.rej_ptype, + self.ns << 4 | self.nr, self.vs << 4 | self.vr, + self.vsa << 4 | self.vra) + + def __len__(self): + return 6 + + def __str__(self): + return super(FrameReject, self).__str__() +\ + " FLAGS={frmr.rej_flags:04b} PTYPE={frmr.rej_ptype:04b}"\ + " N(S)={frmr.ns} N(R)={frmr.nr}"\ + " V(S)={frmr.vs} V(R)={frmr.vr}"\ + " V(SA)={frmr.vsa} V(RA)={frmr.vra}"\ + .format(frmr=self) + + +# ----------------------------------------------------------------------------- +# Service Name Lookup PDU +# ----------------------------------------------------------------------------- +class ServiceNameLookup(ProtocolDataUnit): + name = "SNL" + + def __init__(self, dsap, ssap, sdreq=None, sdres=None): + super(ServiceNameLookup, self).__init__(0b1001, dsap, ssap) + self.sdreq = sdreq if sdreq else list() + self.sdres = sdres if sdres else list() + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + if dsap != 1 or ssap != 1: + raise DecodeError("SSAP and DSAP must be 1 in SNL PDU") + snl_pdu = ServiceNameLookup(dsap, ssap) + offset, size = offset + 2, size - 2 + while size >= 2: + T, L, V = Parameter.decode(data, offset) + if T == Parameter.SDREQ: + snl_pdu.sdreq.append(V) + elif T == Parameter.SDRES: + snl_pdu.sdres.append(V) + else: + log.warning("invalid TLV %r in SNL PDU", (T, L, V)) + offset, size = offset + 2 + L, size - 2 - L + return snl_pdu + + def encode(self): + data = self.encode_header() + for sdreq in self.sdreq: + data += Parameter.encode(Parameter.SDREQ, sdreq) + for sdres in self.sdres: + data += Parameter.encode(Parameter.SDRES, sdres) + return data + + def __len__(self): + return 2 + (len(self.sdres) * 4) \ + + sum([3+len(sdreq[1]) for sdreq in self.sdreq]) + + def __str__(self): + return super(ServiceNameLookup, self).__str__() + \ + " SDRES={0} SDREQ={1}".format(str(self.sdres), str(self.sdreq)) + + +# ----------------------------------------------------------------------------- +# Data Protection Setup PDU +# ----------------------------------------------------------------------------- +class DataProtectionSetup(ProtocolDataUnit): + name = "DPS" + + def __init__(self, dsap, ssap, ecpk=None, rn=None): + super(DataProtectionSetup, self).__init__(0b1010, dsap, ssap) + self.ecpk = ecpk + self.rn = rn + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + if dsap != 0 or ssap != 0: + raise DecodeError("SSAP and DSAP must be 0 in DPS PDU") + dps_pdu = DataProtectionSetup(dsap, ssap) + offset, size = offset + 2, size - 2 + while size >= 2: + T, L, V = Parameter.decode(data, offset) + if T == Parameter.ECPK: + dps_pdu.ecpk = V + elif T == Parameter.RN: + dps_pdu.rn = V + else: + log.debug("unknown TLV %r in DPS PDU", (T, L, V)) + offset, size = offset + 2 + L, size - 2 - L + return dps_pdu + + def encode(self): + if self.dsap != 0 or self.ssap != 0: + raise EncodeError("SSAP and DSAP must be 0 in DPS PDU") + data = self.encode_header() + if self.ecpk: + data += Parameter.encode(Parameter.ECPK, self.ecpk) + if self.rn: + data += Parameter.encode(Parameter.RN, self.rn) + return data + + def __len__(self): + return (2 + + (2 + len(self.ecpk) if self.ecpk else 0) + + (2 + len(self.rn) if self.rn else 0)) + + def __str__(self): + return super(DataProtectionSetup, self).__str__() + \ + " ECPK={0} RN={1}".format( + 'None' if self.ecpk is None else hexlify(self.ecpk).decode(), + 'None' if self.rn is None else hexlify(self.rn).decode()) + + +# ----------------------------------------------------------------------------- +# Information PDU +# ----------------------------------------------------------------------------- +class Information(NumberedProtocolDataUnit): + name = "I" + + def __init__(self, dsap, ssap, ns=None, nr=None, data=None): + super(Information, self).__init__(0b1100, dsap, ssap, ns, nr) + self.data = data if data else b'' + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap, ns, nr = cls.decode_header(data, offset, size) + payload = bytes(data[offset+3:offset+size]) + return cls(dsap, ssap, ns, nr, payload) + + def encode(self): + return self.encode_header() + bytes(self.data) + + def __len__(self): + return 3 + len(self.data) + + def __str__(self): + return (super(Information, self).__str__() + " LEN={0} DATA={1}" + .format(len(self.data), hexlify(self.data))) + + +# ----------------------------------------------------------------------------- +# Receive Ready PDU +# ----------------------------------------------------------------------------- +class ReceiveReady(NumberedProtocolDataUnit): + name = "RR" + + def __init__(self, dsap, ssap, nr=None): + super(ReceiveReady, self).__init__(0b1101, dsap, ssap, 0, nr) + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap, ns, nr = cls.decode_header(data, offset, size) + if ns != 0: + log.warning("reserved bits set in sequence field") + return cls(dsap, ssap, nr) + + def encode(self): + return self.encode_header() + + +# ----------------------------------------------------------------------------- +# Receive Not Ready PDU +# ----------------------------------------------------------------------------- +class ReceiveNotReady(NumberedProtocolDataUnit): + name = "RNR" + + def __init__(self, dsap, ssap, nr): + super(ReceiveNotReady, self).__init__(0b1110, dsap, ssap, 0, nr) + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap, ns, nr = cls.decode_header(data, offset, size) + if ns != 0: + log.warning("reserved bits set in sequence field") + return cls(dsap, ssap, nr) + + def encode(self): + return self.encode_header() + + +# ----------------------------------------------------------------------------- +# UnknownProtocolDataUnit +# ----------------------------------------------------------------------------- +class UnknownProtocolDataUnit(ProtocolDataUnit): + def __init__(self, ptype, dsap, ssap, payload): + super(UnknownProtocolDataUnit, self).__init__(ptype, dsap, ssap) + self.name = "{0:04b}".format(ptype) + self.payload = payload + + @classmethod + def decode(cls, data, offset, size): + dsap, ssap = cls.decode_header(data, offset, size) + pdutype = (data[offset] << 2 | data[offset+1] >> 6) & 0x0F + payload = data[offset+2:offset+size] + return cls(pdutype, dsap, ssap, payload) + + def encode(self): + return self.encode_header() + bytes(self.payload) + + def __len__(self): + return 2 + len(self.payload) + + def __str__(self): + return (super(UnknownProtocolDataUnit, self).__str__() + + " PAYLOAD={}".format(hexlify(self.payload).decode())) + + +# ----------------------------------------------------------------------------- +# pdu decode and encode functions +# ----------------------------------------------------------------------------- +pdu_type_map = { + 0b0000: Symmetry, + 0b0001: ParameterExchange, + 0b0010: AggregatedFrame, + 0b0011: UnnumberedInformation, + 0b0100: Connect, + 0b0101: Disconnect, + 0b0110: ConnectionComplete, + 0b0111: DisconnectedMode, + 0b1000: FrameReject, + 0b1001: ServiceNameLookup, + 0b1010: DataProtectionSetup, + 0b1100: Information, + 0b1101: ReceiveReady, + 0b1110: ReceiveNotReady, +} + + +def decode(data, offset=0, size=None): + size = len(data) if size is None else size + + if offset + size > len(data): + raise DecodeError("size bytes from offset exceed the data length") + if size < 2: + raise DecodeError("less than two header bytes can't make a valid pdu") + + ptype = (struct.unpack_from('>H', data, offset)[0] >> 6) & 0b1111 + pdu_type = pdu_type_map.get(ptype, UnknownProtocolDataUnit) + return pdu_type.decode(data, offset, size) + + +def encode(pdu): + if not isinstance(pdu, ProtocolDataUnit): + raise AttributeError("can't encode %s" % type(pdu)) + + return pdu.encode() diff --git a/src/lib/nfc/llcp/sec.py b/src/lib/nfc/llcp/sec.py new file mode 100644 index 0000000..7fb6bc4 --- /dev/null +++ b/src/lib/nfc/llcp/sec.py @@ -0,0 +1,542 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import struct +import ctypes +import ctypes.util +from ctypes import c_void_p, c_int +from binascii import hexlify + +import logging +log = logging.getLogger(__name__) + +OpenSSL = None + + +class Error(Exception): + pass + + +class EncryptionError(Error): + pass + + +class DecryptionError(Error): + pass + + +class KeyAgreementError(Error): + pass + + +def cipher_suite(name): + if name == "ECDH_anon_WITH_AEAD_AES_128_CCM_4": + return CipherSuite1() + + +class CipherSuite1: + _ccm_t = 4 + _ccm_q = 2 + _ccm_n = 13 + + def __init__(self): + self.random_nonce = None + self.public_key_x = None + self.public_key_y = None + ec_key = OpenSSL.EC_KEY.new_by_curve_name(OpenSSL.NID_X9_62_prime256v1) + if ec_key and ec_key.generate_key() and ec_key.check_key(): + pubkey = ec_key.get_public_key() + x, y = pubkey.get_affine_coordinates_GFp(ec_key.get_group()) + self.public_key_x = x + self.public_key_y = y + self.random_nonce = OpenSSL.rand_bytes(8) + self._ec_key = ec_key + + def calculate_session_key(self, ecpk, rn_i=None, rn_t=None): + if ecpk is None: + raise KeyAgreementError("remote public key is required") + if len(ecpk) != 64: + raise KeyAgreementError("remote public key has wrong size") + if rn_i is None and rn_t is None: + raise KeyAgreementError("remote random nonce is required") + if rn_i and len(rn_i) != 8: + raise KeyAgreementError("initiator random nonce has wrong size") + if rn_t and len(rn_t) != 8: + raise KeyAgreementError("target random nonce has wrong size") + + if rn_i is None: + rn_i = self.random_nonce + if rn_t is None: + rn_t = self.random_nonce + + ec_key = OpenSSL.EC_KEY.new_by_curve_name(OpenSSL.NID_X9_62_prime256v1) + try: + ec_key.set_public_key_affine_coordinates(ecpk[:32], ecpk[32:]) + except AssertionError: + raise KeyAgreementError("remote public key is not on curve") + + cipher = OpenSSL.EVP_aes_128_cbc() + secret = OpenSSL.ECDH(self._ec_key) \ + .compute_key(ec_key.get_public_key()) + k_encr = OpenSSL.CMAC(cipher) \ + .init(rn_i+rn_t) \ + .update(secret).final() + + log.debug("remote ecpk-x %r", hexlify(ecpk[:32])) + log.debug("remote ecpk-y %r", hexlify(ecpk[32:])) + log.debug("shared secret %r", hexlify(secret)) + log.debug("shared nonce %r", hexlify(rn_i+rn_t)) + log.debug("session key %r", hexlify(k_encr)) + + self._pcs = self._pcr = 0 + self._k_encr = k_encr + return self._k_encr + + @property + def icv_size(self): + return self._ccm_t + + def encrypt(self, a, p): + # The nonce N is a leftmost 40-bit fixed part all bits zero + # and a rightmost 64-bit counter part taken from PC(S). + nonce = struct.pack('!xxxxxQ', self._pcs) + if self._pcs < 0xFFFFFFFFFFFFFFFF: + self._pcs += 1 + else: + raise EncryptionError("send counter out of range") + + # The encryption key was computed in calculate_session_key() + key = self._k_encr + + # OpenSSLWrapper methods raise AssertionError when any of the + # operations failed. + try: + return self._encrypt(bytes(a), bytes(p), key, nonce, self._ccm_t) + except AssertionError: + error = "encrypt failed for message %d" % self._pcs + log.error(error) + raise EncryptionError(error) + + @staticmethod + def _encrypt(aad, txt, key, nonce, tlen): + # from https://wiki.openssl.org/index.php/ + # EVP_Authenticated_Encryption_and_Decryption# + # Authenticated_Encryption_using_CCM_mode + evp = OpenSSL.EVP() + evp.encrypt_init(OpenSSL.EVP_aes_128_ccm()) + evp.cipher_ctx.ctrl_set(OpenSSL.EVP.CTRL_CCM_SET_IVLEN, len(nonce)) + evp.cipher_ctx.ctrl_set(OpenSSL.EVP.CTRL_CCM_SET_TAG, tlen) + evp.encrypt_init(key=key, iv=nonce) + evp.encrypt_update(None, None, len(txt)) + evp.encrypt_update(None, aad, len(aad)) + return evp.encrypt_update(len(txt), txt, len(txt)) + \ + evp.cipher_ctx.ctrl_get(OpenSSL.EVP.CTRL_CCM_GET_TAG, tlen) + + def decrypt(self, a, c): + # The nonce N is a leftmost 40-bit fixed part all bits zero + # and a rightmost 64-bit counter part taken from PC(R). + nonce = struct.pack('!xxxxxQ', self._pcr) + if self._pcr < 0xFFFFFFFFFFFFFFFF: + self._pcr += 1 + else: + raise DecryptionError("recv counter out of range") + + # The decryption key was computed in calculate_session_key() + key = self._k_encr + + # OpenSSLWrapper methods raise AssertionError when any of the + # operations failed. + try: + return self._decrypt(bytes(a), bytes(c), key, nonce, self._ccm_t) + except AssertionError: + error = "decrypt failed for message %d" % self._pcr + log.error(error) + raise DecryptionError(error) + + @staticmethod + def _decrypt(aad, txt, key, nonce, tlen): + # from https://wiki.openssl.org/index.php/ + # EVP_Authenticated_Encryption_and_Decryption# + # Authenticated_Decryption_using_CCM_mode + tag = txt[-tlen:] + txt = txt[:-tlen] + evp = OpenSSL.EVP() + evp.decrypt_init(OpenSSL.EVP_aes_128_ccm()) + evp.cipher_ctx.ctrl_set(OpenSSL.EVP.CTRL_CCM_SET_IVLEN, len(nonce)) + evp.cipher_ctx.ctrl_set(OpenSSL.EVP.CTRL_CCM_SET_TAG, len(tag), tag) + evp.decrypt_init(key=key, iv=nonce) + evp.decrypt_update(None, None, len(txt)) + evp.decrypt_update(None, aad, len(aad)) + return evp.decrypt_update(len(txt), txt, len(txt)) + + +class OpenSSLWrapper: + NID_X9_62_prime256v1 = 415 # NIST Curve P-256 + + def __init__(self, libcrypto): + self.crypto = ctypes.CDLL(libcrypto) + self.crypto.BN_new.restype = c_void_p + self.crypto.BN_num_bits.restype = c_int + self.crypto.BN_bn2bin.restype = c_int + self.crypto.BN_bin2bn.restype = c_void_p + self.crypto.BN_free.restype = None + self.crypto.RAND_bytes.restype = c_int + self.crypto.EC_KEY_new_by_curve_name.restype = c_void_p + self.crypto.EC_KEY_generate_key.restype = c_int + self.crypto.EC_KEY_check_key.restype = c_int + self.crypto.EC_KEY_set_public_key.restype = c_int + self.crypto.EC_KEY_set_public_key_affine_coordinates.restype = c_int + self.crypto.EC_KEY_get0_public_key.restype = c_void_p + self.crypto.EC_KEY_get0_group.restype = c_void_p + self.crypto.EC_KEY_free.restype = None + self.crypto.EC_POINT_new.restype = c_void_p + self.crypto.EC_POINT_get_affine_coordinates_GFp.restype = c_int + self.crypto.EC_POINT_set_affine_coordinates_GFp.restype = c_int + self.crypto.EC_POINT_free.restype = None + self.crypto.ECDH_OpenSSL.restype = c_void_p + self.crypto.ECDH_set_method.restype = c_int + self.crypto.ECDH_compute_key.restype = c_int + self.crypto.CMAC_CTX_new.restype = c_void_p + self.crypto.CMAC_CTX_free.restype = None + self.crypto.CMAC_Init.restype = c_int + self.crypto.CMAC_Update.restype = c_int + self.crypto.CMAC_Final.restype = c_int + + self.crypto.EVP_CIPHER_CTX_new.restype = c_void_p + self.crypto.EVP_CIPHER_CTX_init.restype = None + self.crypto.EVP_CIPHER_CTX_ctrl.restype = c_int + self.crypto.EVP_CIPHER_CTX_free.restype = None + + self.crypto.EVP_EncryptInit_ex.restype = c_int + self.crypto.EVP_EncryptUpdate.restype = c_int + self.crypto.EVP_EncryptFinal.restype = c_int + self.crypto.EVP_DecryptInit_ex.restype = c_int + self.crypto.EVP_DecryptUpdate.restype = c_int + self.crypto.EVP_DecryptFinal.restype = c_int + + self.crypto.EVP_aes_128_cbc.restype = c_void_p + self.crypto.EVP_aes_128_cbc.argtypes = [] + self.crypto.EVP_aes_128_ccm.restype = c_void_p + self.crypto.EVP_aes_128_ccm.argtypes = [] + + self.EVP_aes_128_cbc = self.crypto.EVP_aes_128_cbc + self.EVP_aes_128_ccm = self.crypto.EVP_aes_128_ccm + + class BIGNUM: + def __init__(self, bignum, release=False): + self._bignum = bignum + self._release = release + + def __del__(self): + if self._release: + OpenSSL.crypto.BN_free(self) + + @property + def _as_parameter_(self): + return c_void_p(self._bignum) + + @staticmethod + def new(): + # BIGNUM *BN_new(void); + bignum = OpenSSL.crypto.BN_new() + if bignum is None: + log.error("BN_new") + else: + return OpenSSL.BIGNUM(bignum, release=True) + + def num_bits(self): + return OpenSSL.crypto.BN_num_bits(self) + + def num_bytes(self): + return (self.num_bits() + 7) // 8 + + def bn2bin(self, num_bytes=None): + # int BN_bn2bin(const BIGNUM *a, unsigned char *to); + if num_bytes is None: + num_bytes = self.num_bytes() + else: + assert num_bytes >= self.num_bytes(), "bn2bin num bytes" + strbuf = ctypes.create_string_buffer(num_bytes) + OpenSSL.crypto.BN_bn2bin(self, strbuf) + return strbuf.raw + + @staticmethod + def bin2bn(s): + # BIGNUM *BN_bin2bn(const unsigned char *s, int len, BIGNUM *ret); + strbuf = ctypes.create_string_buffer(bytes(s), len(s)) + res = OpenSSL.crypto.BN_bin2bn(strbuf, len(s), None) + if res is None: + log.error("BN_bin2bn") + else: + return OpenSSL.BIGNUM(res) + + def rand_bytes(self, num): + # int RAND_bytes(unsigned char *buf, int num); + buf = ctypes.create_string_buffer(num) + res = self.crypto.RAND_bytes(buf, c_int(num)) + if res == 0: + log.error("RAND_bytes") + else: + return buf.raw + + class EC_KEY: + def __init__(self, ec_key): + self._ec_key = ec_key + + def __del__(self): + OpenSSL.crypto.EC_KEY_free(self) + + @property + def _as_parameter_(self): + return c_void_p(self._ec_key) + + @staticmethod + def new_by_curve_name(nid): + # EC_KEY *EC_KEY_new_by_curve_name(int nid); + res = OpenSSL.crypto.EC_KEY_new_by_curve_name(c_int(nid)) + if res is None: + log.error("EC_KEY_new_by_curve_name") + else: + return OpenSSL.EC_KEY(res) + + def generate_key(self): + # int EC_KEY_generate_key(EC_KEY *key); + res = OpenSSL.crypto.EC_KEY_generate_key(self) + if res == 0: + log.error("EC_KEY_generate_key") + return bool(res) + + def check_key(self): + # int EC_KEY_check_key(const EC_KEY *key); + res = OpenSSL.crypto.EC_KEY_check_key(self) + if res == 0: + log.error("EC_KEY_check_key") + return bool(res) + + def set_public_key_affine_coordinates(self, pubkey_x, pubkey_y): + # int EC_KEY_set_public_key_affine_coordinates(EC_KEY *key, + # BIGNUM *x, BIGNUM *y); + r = OpenSSL.crypto.EC_KEY_set_public_key_affine_coordinates( + self, *list(map(OpenSSL.BIGNUM.bin2bn, (pubkey_x, pubkey_y)))) + if r != 1: + errmsg = "EC_KEY_set_public_key_affine_coordinates" + raise AssertionError(errmsg) + + def get_public_key(self): + # const EC_POINT *EC_KEY_get0_public_key(const EC_KEY *key); + res = OpenSSL.crypto.EC_KEY_get0_public_key(self) + if res is None: + log.error("EC_KEY_get0_public_key") + else: + return OpenSSL.EC_POINT(res) + + def get_group(self): + # const EC_GROUP *EC_KEY_get0_group(const EC_KEY *key); + res = OpenSSL.crypto.EC_KEY_get0_group(self) + if res is None: + log.error("EC_KEY_get0_group") + else: + return OpenSSL.EC_GROUP(res) + + class EC_GROUP: + def __init__(self, ec_group): + self._ec_group = ec_group + + @property + def _as_parameter_(self): + return c_void_p(self._ec_group) + + class EC_POINT: + def __init__(self, ec_point): + self._ec_point = ec_point + + @property + def _as_parameter_(self): + return c_void_p(self._ec_point) + + def get_affine_coordinates_GFp(self, ec_group): + # int EC_POINT_get_affine_coordinates_GFp(const EC_GROUP *group, + # const EC_POINT *p, BIGNUM *x, BIGNUM *y, BN_CTX *ctx); + x, y = (OpenSSL.BIGNUM.new(), OpenSSL.BIGNUM.new()) + func = OpenSSL.crypto.EC_POINT_get_affine_coordinates_GFp + res = func(ec_group, self, x, y, None) + if res == 0: + log.error("EC_POINT_get_affine_coordinates_GFp") + else: + return (x.bn2bin(32), y.bn2bin(32)) + + class ECDH: + def __init__(self, local_key): + self.key = local_key + method = OpenSSL.crypto.ECDH_OpenSSL() + OpenSSL.crypto.ECDH_set_method(self.key, c_void_p(method)) + + def compute_key(self, pub_key): + # int ECDH_compute_key(void *out, size_t outlen, + # const EC_POINT *pub_key, EC_KEY *ecdh, + # void *(*KDF)(const void *in, size_t inlen, + # void *out, size_t *outlen)); + strbuf = ctypes.create_string_buffer(32) + args = (strbuf, 32, pub_key, self.key, None) + r = OpenSSL.crypto.ECDH_compute_key(*args) + assert r == 32, "ECDH_compute_key" + return strbuf.raw # the shared secret z + + class CMAC: + def __init__(self, cipher): + # CMAC_CTX *CMAC_CTX_new(void); + self._cmac_ctx = OpenSSL.crypto.CMAC_CTX_new() + self._cipher = cipher + + def __del__(self): + # void CMAC_CTX_free(CMAC_CTX *ctx); + OpenSSL.crypto.CMAC_CTX_free(self) + + @property + def _as_parameter_(self): + return c_void_p(self._cmac_ctx) + + def init(self, key): + # int CMAC_Init(CMAC_CTX *ctx, const void *key, size_t keylen, + # const EVP_CIPHER *cipher, ENGINE *impl); + assert len(key) == 16 + keybuf = ctypes.create_string_buffer(key, 16) + keylen = ctypes.c_size_t(16) + cipher = ctypes.c_void_p(self._cipher) + r = OpenSSL.crypto.CMAC_Init(self, keybuf, keylen, cipher, None) + assert r == 1, "CMAC_Init" + return self + + def update(self, msg): + # int CMAC_Update(CMAC_CTX *ctx, const void *data, size_t dlen); + msgbuf = ctypes.create_string_buffer(msg, len(msg)) + msglen = ctypes.c_size_t(len(msg)) + r = OpenSSL.crypto.CMAC_Update(self, msgbuf, msglen) + assert r == 1, "CMAC_Update" + return self + + def final(self): + macbuf = ctypes.create_string_buffer(16) + maclen = ctypes.c_size_t(0) + rc = OpenSSL.crypto.CMAC_Final(self, macbuf, ctypes.byref(maclen)) + assert rc == 1 and maclen.value == 16, "CMAC_Final" + return macbuf.raw + + class EVP: + CTRL_CCM_SET_IVLEN = 0x09 + CTRL_CCM_GET_TAG = 0x10 + CTRL_CCM_SET_TAG = 0x11 + CTRL_CCM_SET_L = 0x14 + + class CIPHER_CTX: + def __init__(self): + # EVP_CIPHER_CTX *EVP_CIPHER_CTX_new(void); + ctx = OpenSSL.crypto.EVP_CIPHER_CTX_new() + if ctx is None: + raise AssertionError("EVP_CIPHER_CTX_new") + self._ctx = ctx + + def __del__(self): + # void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *ctx); + OpenSSL.crypto.EVP_CIPHER_CTX_free(self) + + @property + def _as_parameter_(self): + return c_void_p(self._ctx) + + def ctrl_set(self, op, arg, ptr=None): + # int EVP_CIPHER_CTX_ctrl(EVP_CIPHER_CTX *ctx, int type, + # int arg, void *ptr); + r = OpenSSL.crypto.EVP_CIPHER_CTX_ctrl(self, op, arg, ptr) + if r != 1: + raise AssertionError("EVP_CIPHER_CTX_ctrl") + + def ctrl_get(self, op, arg): + # int EVP_CIPHER_CTX_ctrl(EVP_CIPHER_CTX *ctx, int type, + # int arg, void *ptr); + outbuf = ctypes.create_string_buffer(arg) + r = OpenSSL.crypto.EVP_CIPHER_CTX_ctrl(self, op, arg, outbuf) + if r != 1: + raise AssertionError("EVP_CIPHER_CTX_ctrl") + return outbuf.raw + + def __init__(self, evp_cipher_ctx=None): + if evp_cipher_ctx: + self._ctx = evp_cipher_ctx + else: + self._ctx = OpenSSL.EVP.CIPHER_CTX() + + @property + def cipher_ctx(self): + return self._ctx + + def encrypt_init(self, evp_cipher=None, key=None, iv=None): + # int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx, + # const EVP_CIPHER *type, ENGINE *impl, + # unsigned char *key, unsigned char *iv); + r = OpenSSL.crypto.EVP_EncryptInit_ex( + self._ctx, c_void_p(evp_cipher), None, key, iv) + if r != 1: + raise AssertionError("EVP_EncryptInit_ex") + + def encrypt_update(self, out_len, message, msg_len): + # int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, + # int *outl, unsigned char *in, int inl); + if out_len is None: + out_buf = None + out_len = c_int(0) + else: + out_buf = ctypes.create_string_buffer(out_len) + out_len = c_int(out_len) + r = OpenSSL.crypto.EVP_EncryptUpdate( + self._ctx, out_buf, ctypes.byref(out_len), message, msg_len) + if r != 1: + raise AssertionError("EVP_EncryptUpdate") + return out_buf.raw[0:out_len.value] if out_buf else b'' + + def decrypt_init(self, evp_cipher=None, key=None, iv=None): + # int EVP_DecryptInit_ex(EVP_CIPHER_CTX *ctx, + # const EVP_CIPHER *type, ENGINE *impl, + # unsigned char *key, unsigned char *iv); + r = OpenSSL.crypto.EVP_DecryptInit_ex( + self._ctx, c_void_p(evp_cipher), None, key, iv) + if r != 1: + raise AssertionError("EVP_DecryptInit_ex") + + def decrypt_update(self, out_len, message, msg_len): + # int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, + # int *outl, unsigned char *in, int inl); + if out_len is None: + out_buf = None + out_len = c_int(0) + else: + out_buf = ctypes.create_string_buffer(out_len) + out_len = c_int(out_len) + r = OpenSSL.crypto.EVP_DecryptUpdate( + self._ctx, out_buf, ctypes.byref(out_len), message, msg_len) + if r != 1: + raise AssertionError("EVP_DecryptUpdate") + return out_buf.raw[0:out_len.value] if out_buf else b'' + + +libcrypto = ctypes.util.find_library('crypto.so.1.0') +if libcrypto is not None: + OpenSSL = OpenSSLWrapper(libcrypto) diff --git a/src/lib/nfc/llcp/socket.py b/src/lib/nfc/llcp/socket.py new file mode 100644 index 0000000..a51d915 --- /dev/null +++ b/src/lib/nfc/llcp/socket.py @@ -0,0 +1,177 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- + + +class Socket(object): + """ + Create a new LLCP socket with the given socket type. The + socket type should be one of: + + * :const:`nfc.llcp.LOGICAL_DATA_LINK` for best-effort + communication using LLCP connection-less PDU exchange + + * :const:`nfc.llcp.DATA_LINK_CONNECTION` for reliable + communication using LLCP connection-mode PDU exchange + + * :const:`nfc.llcp.llc.RAW_ACCESS_POINT` for unregulated LLCP PDU + exchange (useful to implement test programs) + """ + def __init__(self, llc, sock_type): + self._tco = None if sock_type is None else llc.socket(sock_type) + self._llc = llc + + @property + def llc(self): + """The :class:`~nfc.llcp..llc.LogicalLinkController` instance + to which this socket belongs. This attribute is read-only.""" + return self._llc + + def resolve(self, name): + """Resolve a service name into an address. This may involve + conversation with the remote service discovery component if + the name is hasn't yet been resolved. The return value is the + service access point address for the service name bound at the + remote device. The address value 0 indicates that the remote + device does not have a service with the requested name. The + address value 1 indicates that the remote device has a data + link connection service with the requested name that can only + be connected by service name. The return value is None when + communication with the peer device terminated while waiting + for a response. + + """ + return self.llc.resolve(name) + + def setsockopt(self, option, value): + """Set the value of the given socket option and return the + current value which may have been corrected if it was out of + bounds.""" + return self.llc.setsockopt(self._tco, option, value) + + def getsockopt(self, option): + """Return the value of the given socket option.""" + return self.llc.getsockopt(self._tco, option) + + def bind(self, address=None): + """Bind the socket to address. The socket must not already be + bound. The address may be a service name string, a service + access point number, or it may be omitted. If address is a + well-known service name the socket will be bound to the + corresponding service access point address, otherwise the + socket will be bound to the next available service access + point address between 16 and 31 (inclusively). If address is a + number between 32 and 63 (inclusively) the socket will be + bound to that service access point address. If the address + argument is omitted the socket will be bound to the next + available service access point address between 32 and 63.""" + return self.llc.bind(self._tco, address) + + def connect(self, address): + """Connect to a remote socket at address. Address may be a + service name string or a service access point number.""" + return self.llc.connect(self._tco, address) + + def listen(self, backlog): + """Mark a socket as a socket that will be used to accept + incoming connection requests using accept(). The *backlog* + defines the maximum length to which the queue of pending + connections for the socket may grow. A backlog of zero + disables queuing of connection requests. + """ + return self.llc.listen(self._tco, backlog) + + def accept(self): + """Accept a connection. The socket must be bound to an address + and listening for connections. The return value is a new + socket object usable to send and receive data on the + connection.""" + socket = Socket(self._llc, None) + socket._tco = self.llc.accept(self._tco) + return socket + + def send(self, data, flags=0): + """Send data to the socket. The socket must be connected to a remote + socket. Returns a boolean value that indicates success or + failure. A false value is typically an indication that the + socket or connection was closed. + + """ + return self.llc.send(self._tco, data, flags) + + def sendto(self, data, addr, flags=0): + """Send data to the socket. The socket should not be connected + to a remote socket, since the destination socket is specified + by addr. Returns a boolean value that indicates success + or failure. Failure to send is generally an indication that + the socket was closed.""" + return self.llc.sendto(self._tco, data, addr, flags) + + def recv(self): + """Receive data from the socket. The return value is a bytes object + representing the data received. The maximum amount of data + that may be returned is determined by the link or connection + maximum information unit size.""" + return self.llc.recv(self._tco) + + def recvfrom(self): + """Receive data from the socket. The return value is a pair + (bytes, address) where string is a string representing the + data received and address is the address of the socket sending + the data.""" + return self.llc.recvfrom(self._tco) + + def poll(self, event, timeout=None): + """Wait for a socket event. Posssible *event* values are the strings + "recv", "send" and "acks". Whent the timeout is present and + not :const:`None`, it should be a floating point number + specifying the timeout for the operation in seconds (or + fractions thereof). For "recv" or "send" the :meth:`poll` + method returns :const:`True` if a next :meth:`recv` or + :meth:`send` operation would be non-blocking. The "acks" event + may only be used with a data-link-connection type socket; the + call then returns :const:`True` if the counter of received + acknowledgements was greater than zero and decrements the + counter by one. + + """ + return self.llc.poll(self._tco, event, timeout) + + def getsockname(self): + """Obtain the address to which the socket is bound. For an + unbound socket the returned value is None. + """ + return self.llc.getsockname(self._tco) + + def getpeername(self): + """Obtain the address of the peer connected on the socket. For + an unconnected socket the returned value is None. + """ + return self.llc.getpeername(self._tco) + + def close(self): + """Close the socket. All future operations on the socket + object will fail. The remote end will receive no more data + Sockets are automatically closed when the logical link + controller terminates (gracefully or by link disruption). A + connection-mode socket will attempt to disconnect the data + link connection (if in connected state).""" + return self.llc.close(self._tco) diff --git a/src/lib/nfc/llcp/tco.py b/src/lib/nfc/llcp/tco.py new file mode 100644 index 0000000..3c049eb --- /dev/null +++ b/src/lib/nfc/llcp/tco.py @@ -0,0 +1,733 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +from . import pdu +from . import err +import src.lib.nfc.llcp + +import errno +import threading +import collections + +import logging +log = logging.getLogger(__name__) + + +class TransmissionControlObject(object): + class State(object): + def __init__(self): + self.names = ("SHUTDOWN", "CLOSED", "LISTEN", "CONNECT", + "ESTABLISHED", "DISCONNECT", "CLOSE_WAIT") + self.value = self.names.index("SHUTDOWN") + + def __str__(self): + return self.names[self.value] + + def __getattr__(self, name): + return self.value == self.names.index(name) + + def __setattr__(self, name, value): + if name not in ("names", "value"): + value, name = self.names.index(name), "value" + object.__setattr__(self, name, value) + + class Mode(object): + def __init__(self): + self.names = ("BLOCK", "SEND_BUSY", "RECV_BUSY", "RECV_BUSY_SENT") + self.value = dict([(name, False) for name in self.names]) + + def __str__(self): + return str(self.value) + + def __getattr__(self, name): + return self.value[name] + + def __init__(self, send_miu, recv_miu): + self.lock = threading.RLock() + self.mode = TransmissionControlObject.Mode() + self.state = TransmissionControlObject.State() + self.send_queue = collections.deque() + self.recv_queue = collections.deque() + self.send_ready = threading.Condition(self.lock) + self.recv_ready = threading.Condition(self.lock) + self.recv_miu = recv_miu + self.send_miu = send_miu + self.recv_buf = 1 + self.send_buf = 1 + self.addr = None + self.peer = None + + @property + def is_bound(self): + return self.addr is not None + + def setsockopt(self, option, value): + if option == nfc.llcp.SO_SNDBUF: + # with self.lock: self.send_buf = int(value) + # adjustable send buffer only with non-blocking socket mode + raise NotImplementedError("SO_SNDBUF can not be set") + elif option == nfc.llcp.SO_RCVBUF: + with self.lock: + self.recv_buf = int(value) + else: + raise ValueError("invalid option value") + + def getsockopt(self, option): + if option == nfc.llcp.SO_SNDMIU: + return self.send_miu + if option == nfc.llcp.SO_RCVMIU: + return self.recv_miu + if option == nfc.llcp.SO_SNDBUF: + return self.send_buf + if option == nfc.llcp.SO_RCVBUF: + return self.recv_buf + + def bind(self, addr): + if self.addr and addr and self.addr != addr: + log.warning("socket rebound from {} to {}".format(self.addr, addr)) + self.addr = addr + return self.addr + + def poll(self, event, timeout): + if event == "recv": + with self.recv_ready: + if len(self.recv_queue) == 0: + self.recv_ready.wait(timeout) + if len(self.recv_queue) > 0: + return self.recv_queue[0] + return None + if event == "send": + with self.send_ready: + if len(self.send_queue) >= self.send_buf: + self.send_ready.wait(timeout) + return len(self.send_queue) < self.send_buf + + def send(self, send_pdu, flags): + with self.send_ready: + self.send_queue.append(send_pdu) + if not (flags & nfc.llcp.MSG_DONTWAIT): + self.send_ready.wait() + + def recv(self): + with self.recv_ready: + try: + return self.recv_queue.popleft() + except IndexError: + self.recv_ready.wait() + return self.recv_queue.popleft() + + def close(self): + with self.lock: + self.send_queue.clear() + self.recv_queue.clear() + self.send_ready.notify_all() + self.recv_ready.notify_all() + self.state.SHUTDOWN = True + + # + # enqueue() and dequeue() are called from llc run thread + # + def enqueue(self, rcvd_pdu): + with self.lock: + if len(self.recv_queue) < self.recv_buf: + log.debug("enqueue {0}".format(rcvd_pdu)) + self.recv_queue.append(rcvd_pdu) + self.recv_ready.notify() + return True + else: + log.warning("discard {0}".format(rcvd_pdu)) + return False + + def dequeue(self, miu_size, icv_size, notify=True): + # Return the first pending outbound PDU if it's information + # field size (total size - header size) does not exceed the + # given miu_size value. For UI and I PDUs do also consider the + # icv_size value (this is set to non-zero by the packet + # collector when aggregating). Re-insert the PDU at the + # beginning of the send queue if it exceeds the miu_size. + # Skip the length check if miu_size is None. + with self.lock: + try: + send_pdu = self.send_queue.popleft() + log.debug("dequeue {0}".format(send_pdu)) + except IndexError: + return None + + if send_pdu.name in ("UI", "I"): + pdu_size = len(send_pdu) + icv_size + else: + pdu_size = len(send_pdu) + + if ((miu_size is not None and + pdu_size - send_pdu.header_size > miu_size)): + log.debug("requeue {0}".format(send_pdu)) + self.send_queue.appendleft(send_pdu) + return None + + if notify is True: + self.send_ready.notify() + + return send_pdu + + +class RawAccessPoint(TransmissionControlObject): + """ + ============= =========== ============ + State Event Transition + ============= =========== ============ + SHUTDOWN init() ESTABLISHED + ESTABLISHED close() SHUTDOWN + ============= =========== ============ + """ + def __init__(self, recv_miu): + super(RawAccessPoint, self).__init__(128, recv_miu) + self.state.ESTABLISHED = True + + def __str__(self): + return "RAW {:2} -> ?".format(self.addr + if self.addr is not None + else "None") + + def setsockopt(self, option, value): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + super(RawAccessPoint, self).setsockopt(option, value) + + def getsockopt(self, option): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + return super(RawAccessPoint, self).getsockopt(option) + + def poll(self, event, timeout): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + if event not in ("recv", "send"): + raise err.Error(errno.EINVAL) + return super(RawAccessPoint, self).poll(event, timeout) is not None + + def send(self, send_pdu, flags): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + log.debug("{0} send {1}".format(str(self), send_pdu)) + super(RawAccessPoint, self).send(send_pdu, flags) + return self.state.ESTABLISHED is True + + def recv(self): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + try: + return super(RawAccessPoint, self).recv() + except IndexError: + raise err.Error(errno.EPIPE) + + def close(self): + super(RawAccessPoint, self).close() + + # + # enqueue() and dequeue() are called from llc run thread + # + def enqueue(self, rcvd_pdu): + return super(RawAccessPoint, self).enqueue(rcvd_pdu) + + def dequeue(self, miu_size, icv_size): + return super(RawAccessPoint, self).dequeue(miu_size=None, icv_size=0) + + +class LogicalDataLink(TransmissionControlObject): + """ + ============= =========== ============ + State Event Transition + ============= =========== ============ + SHUTDOWN init() ESTABLISHED + ESTABLISHED close() SHUTDOWN + ============= =========== ============ + """ + def __init__(self, recv_miu): + super(LogicalDataLink, self).__init__(128, recv_miu) + self.state.ESTABLISHED = True + + def __str__(self): + return "LDL {addr:2} -> {peer:2}".format( + addr=self.addr if self.addr is not None else "None", + peer=self.peer if self.peer is not None else "None" + ) + + def setsockopt(self, option, value): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + super(LogicalDataLink, self).setsockopt(option, value) + + def getsockopt(self, option): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + return super(LogicalDataLink, self).getsockopt(option) + + def connect(self, dest): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + with self.lock: + self.peer = dest + return self.peer > 0 + + def poll(self, event, timeout): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + if event not in ("recv", "send"): + raise err.Error(errno.EINVAL) + return super(LogicalDataLink, self).poll(event, timeout) is not None + + def sendto(self, message, dest, flags): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + if self.peer and dest != self.peer: + raise err.Error(errno.EDESTADDRREQ) + if len(message) > self.send_miu: + raise err.Error(errno.EMSGSIZE) + send_pdu = pdu.UnnumberedInformation(dest, self.addr, data=message) + super(LogicalDataLink, self).send(send_pdu, flags) + return self.state.ESTABLISHED is True + + def recvfrom(self): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + try: + rcvd_pdu = super(LogicalDataLink, self).recv() + except IndexError: + raise err.Error(errno.EPIPE) + return (rcvd_pdu.data, rcvd_pdu.ssap) if rcvd_pdu else (None, None) + + def close(self): + super(LogicalDataLink, self).close() + + # + # enqueue() and dequeue() are called from llc run thread + # + def enqueue(self, rcvd_pdu): + if not rcvd_pdu.name == "UI": + log.warning("ignore %s PDU on logical data link", rcvd_pdu.name) + return False + if len(rcvd_pdu.data) > self.recv_miu: + log.warning("received UI PDU exceeds local link MIU") + return False + return super(LogicalDataLink, self).enqueue(rcvd_pdu) + + def dequeue(self, miu_size, icv_size): + return super(LogicalDataLink, self).dequeue(miu_size, icv_size) + + +class DataLinkConnection(TransmissionControlObject): + """ + ============= =========== ============ + State Event Transition + ============= =========== ============ + SHUTDOWN init() ESTABLISHED + CLOSED listen() LISTEN + CLOSED connect() CONNECT + CONNECT CC-PDU ESTABLISHED + CONNECT DM-PDU CLOSED + ESTABLISHED I-PDU ESTABLISHED + ESTABLISHED RR-PDU ESTABLISHED + ESTABLISHED RNR-PDU ESTABLISHED + ESTABLISHED FRMR-PDU SHUTDOWN + ESTABLISHED DISC-PDU CLOSE_WAIT + ESTABLISHED close() SHUTDOWN + CLOSE_WAIT close() SHUTDOWN + ============= =========== ============ + """ + + DLC_PDU_NAMES = ("CONNECT", "DISC", "CC", "DM", "FRMR", "I", "RR", "RNR") + + def __init__(self, recv_miu, recv_win): + super(DataLinkConnection, self).__init__(128, recv_miu) + self.state.CLOSED = True + self.acks_ready = threading.Condition(self.lock) + self.acks_recvd = 0 # received acknowledgements + self.recv_confs = 0 # outstanding receive confirmations + self.send_token = threading.Condition(self.lock) + self.recv_buf = recv_win + self.recv_win = recv_win # RW(Local) + self.recv_cnt = 0 # V(R) + self.recv_ack = 0 # V(RA) + self.send_win = None # RW(Remote) + self.send_cnt = 0 # V(S) + self.send_ack = 0 # V(SA) + + def __str__(self): + s = "DLC {addr:2} <-> {peer:2} {dlc.state} " + s += "RW(R)={dlc.send_win} V(S)={dlc.send_cnt} V(SA)={dlc.send_ack} " + s += "RW(L)={dlc.recv_win} V(R)={dlc.recv_cnt} V(RA)={dlc.recv_ack}" + return s.format( + dlc=self, + addr=self.addr if self.addr is not None else "None", + peer=self.peer if self.peer is not None else "None" + ) + + def log(self, string): + log.debug("DLC ({dlc.addr},{dlc.peer}) {dlc.state} {s}" + .format(dlc=self, s=string)) + + def err(self, string): + log.error("DLC ({dlc.addr},{dlc.peer}) {s}".format(dlc=self, s=string)) + + def setsockopt(self, option, value): + with self.lock: + if option == nfc.llcp.SO_RCVMIU and self.state.CLOSED: + self.recv_miu = min(value, 2175) + return + if option == nfc.llcp.SO_RCVBUF and self.state.CLOSED: + self.recv_win = min(value, 15) + self.recv_buf = self.recv_win + return + if option == nfc.llcp.SO_RCVBSY: + self.mode.RECV_BUSY = bool(value) + return + super(DataLinkConnection, self).setsockopt(option, value) + + def getsockopt(self, option): + if option == nfc.llcp.SO_RCVBUF: + return self.recv_win + if option == nfc.llcp.SO_SNDBSY: + return self.mode.SEND_BUSY + if option == nfc.llcp.SO_RCVBSY: + return self.mode.RECV_BUSY + return super(DataLinkConnection, self).getsockopt(option) + + def listen(self, backlog): + with self.lock: + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + if not self.state.CLOSED: + self.err("listen() but socket state is {0}".format(self.state)) + raise err.Error(errno.ENOTSUP) + self.state.LISTEN = True + self.recv_buf = backlog + + def accept(self): + with self.lock: + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + if not self.state.LISTEN: + self.err("accept() but socket state is {0}".format(self.state)) + raise err.Error(errno.EINVAL) + self.recv_buf += 1 + try: + rcvd_pdu = super(DataLinkConnection, self).recv() + except IndexError: + raise err.Error(errno.EPIPE) + self.recv_buf -= 1 + if rcvd_pdu.name == "CONNECT": + dlc = DataLinkConnection(self.recv_miu, self.recv_win) + dlc.addr = self.addr + dlc.peer = rcvd_pdu.ssap + dlc.send_miu = rcvd_pdu.miu + dlc.send_win = rcvd_pdu.rw + send_pdu = pdu.ConnectionComplete(dlc.peer, dlc.addr) + send_pdu.miu, send_pdu.rw = dlc.recv_miu, dlc.recv_win + log.debug("accepting CONNECT from SAP %d" % dlc.peer) + dlc.state.ESTABLISHED = True + self.send_queue.append(send_pdu) + return dlc + else: # pragma: no cover + raise RuntimeError("CONNECT expected, not " + rcvd_pdu.name) + + def connect(self, dest): + with self.lock: + if not self.state.CLOSED: + self.err("connect() in socket state {0}".format(self.state)) + if self.state.ESTABLISHED: + raise err.Error(errno.EISCONN) + if self.state.CONNECT: + raise err.Error(errno.EALREADY) + raise err.Error(errno.EPIPE) + if isinstance(dest, (bytes, bytearray)): + send_pdu = pdu.Connect(1, self.addr, self.recv_miu, + self.recv_win, bytes(dest)) + elif isinstance(dest, str): + send_pdu = pdu.Connect(1, self.addr, self.recv_miu, + self.recv_win, dest.encode('latin')) + elif isinstance(dest, int): + send_pdu = pdu.Connect(dest, self.addr, self.recv_miu, + self.recv_win) + else: + raise TypeError("connect destination must be int or bytes") + + self.state.CONNECT = True + self.send_queue.append(send_pdu) + + try: + rcvd_pdu = super(DataLinkConnection, self).recv() + except IndexError: + raise err.Error(errno.EPIPE) + + if rcvd_pdu.name == "DM": + logstr = "connect rejected with reason {}" + self.log(logstr.format(rcvd_pdu.reason)) + self.state.CLOSED = True + raise err.ConnectRefused(rcvd_pdu.reason) + elif rcvd_pdu.name == "CC": + self.peer = rcvd_pdu.ssap + self.recv_buf = self.recv_win + self.send_miu = rcvd_pdu.miu + self.send_win = rcvd_pdu.rw + self.state.ESTABLISHED = True + return + else: # pragma: no cover + raise RuntimeError("CC or DM expected, not " + rcvd_pdu.name) + + @property + def send_window_slots(self): + # RW(R) - V(S) + V(SA) mod 16 + return (self.send_win - self.send_cnt + self.send_ack) % 16 + + @property + def recv_window_slots(self): + # RW(L) - V(R) + V(RA) mod 16 + return (self.recv_win - self.recv_cnt + self.recv_ack) % 16 + + def send(self, message, flags): + with self.send_token: + if not self.state.ESTABLISHED: + self.err("send() in socket state {0}".format(self.state)) + if self.state.CLOSE_WAIT: + raise err.Error(errno.EPIPE) + raise err.Error(errno.ENOTCONN) + if len(message) > self.send_miu: + raise err.Error(errno.EMSGSIZE) + while self.send_window_slots == 0 and self.state.ESTABLISHED: + if flags & nfc.llcp.MSG_DONTWAIT: + raise err.Error(errno.EWOULDBLOCK) + self.log("waiting on busy send window") + self.send_token.wait() + self.log("send {0} byte on {1}".format(len(message), str(self))) + if self.state.ESTABLISHED: + send_pdu = pdu.Information(self.peer, self.addr, data=message) + send_pdu.ns = self.send_cnt + self.send_cnt = (self.send_cnt + 1) % 16 + super(DataLinkConnection, self).send(send_pdu, flags) + return self.state.ESTABLISHED is True + + def recv(self): + with self.lock: + if not (self.state.ESTABLISHED or self.state.CLOSE_WAIT): + self.err("recv() in socket state {0}".format(self.state)) + raise err.Error(errno.ENOTCONN) + + try: + rcvd_pdu = super(DataLinkConnection, self).recv() + except IndexError: + return None + + if rcvd_pdu.name == "I": + self.recv_confs += 1 + if self.recv_confs > self.recv_win: + self.err("recv_confs({0}) > recv_win({1})" + .format(self.recv_confs, self.recv_win)) + raise RuntimeError("recv_confs > recv_win") + return rcvd_pdu.data + + if rcvd_pdu.name == "DISC": + self.close() + return None + + raise RuntimeError("only I or DISC expected, not " + rcvd_pdu.name) + + def poll(self, event, timeout): + if self.state.SHUTDOWN: + raise err.Error(errno.ESHUTDOWN) + + if event == "recv": + if self.state.ESTABLISHED or self.state.CLOSE_WAIT: + rcvd_pdu = super(DataLinkConnection, self).poll(event, timeout) + if self.state.ESTABLISHED or self.state.CLOSE_WAIT: + return isinstance(rcvd_pdu, pdu.Information) + elif event == "send": + if self.state.ESTABLISHED: + if super(DataLinkConnection, self).poll(event, timeout): + return self.state.ESTABLISHED + return False + elif event == "acks": + with self.acks_ready: + if not self.acks_recvd > 0: + self.acks_ready.wait(timeout) + if self.acks_recvd > 0: + self.acks_recvd = self.acks_recvd - 1 + return True + return False + else: + raise err.Error(errno.EINVAL) + + def close(self): + with self.lock: + self.log("close()") + if self.state.ESTABLISHED and self.is_bound: + self.state.DISCONNECT = True + self.send_token.notify_all() + self.acks_ready.notify_all() + send_pdu = pdu.Disconnect(self.peer, self.addr) + self.send_queue.append(send_pdu) + try: + super(DataLinkConnection, self).recv() + except IndexError: + pass + super(DataLinkConnection, self).close() + self.acks_ready.notify_all() + self.send_token.notify_all() + + # + # enqueue() and dequeue() are called from llc thread context + # + def enqueue(self, rcvd_pdu): + self.log("enqueue {pdu.name} PDU".format(pdu=rcvd_pdu)) + + if rcvd_pdu.name not in self.DLC_PDU_NAMES: + self.err("non connection mode pdu on data link connection") + send_pdu = pdu.FrameReject.from_pdu(rcvd_pdu, flags="W", dlc=self) + self.close() + self.send_queue.append(send_pdu) + return + + if self.state.CLOSED: + self.send_queue.append(pdu.DisconnectedMode( + rcvd_pdu.ssap, rcvd_pdu.dsap, reason=1)) + + elif self.state.LISTEN and rcvd_pdu.name == "CONNECT": + if super(DataLinkConnection, self).enqueue(rcvd_pdu) is False: + log.warning("full backlog on listening socket") + self.send_queue.append(pdu.DisconnectedMode( + rcvd_pdu.ssap, rcvd_pdu.dsap, reason=0x20)) + + elif self.state.CONNECT and rcvd_pdu.name in ("CC", "DM"): + with self.lock: + self.recv_queue.append(rcvd_pdu) + self.recv_ready.notify() + + elif self.state.DISCONNECT and rcvd_pdu.name == "DM": + with self.lock: + self.recv_queue.append(rcvd_pdu) + self.recv_ready.notify() + + elif self.state.ESTABLISHED: + return self._enqueue_state_established(rcvd_pdu) + + def _enqueue_state_established(self, rcvd_pdu): + if rcvd_pdu.name == "I": + frmr = None + if len(rcvd_pdu.data) > self.recv_miu: + frmr = pdu.FrameReject.from_pdu(rcvd_pdu, flags="I", dlc=self) + elif rcvd_pdu.ns != self.recv_cnt: + frmr = pdu.FrameReject.from_pdu(rcvd_pdu, flags="S", dlc=self) + if frmr: + self.log("reject " + str(self)) + self.send_queue.clear() + self.send_queue.append(frmr) + log.debug("enqueued frame reject pdu") + return + + if rcvd_pdu.name == "FRMR": + with self.lock: + self.state.SHUTDOWN = True + self.close() + return + + if rcvd_pdu.name == "DISC": + with self.lock: + self.state.CLOSE_WAIT = True + self.send_queue.clear() + self.send_queue.append(pdu.DisconnectedMode( + self.peer, self.addr, reason=0)) + return + + if rcvd_pdu.name in ("I", "RR", "RNR"): + with self.lock: + # acks = N(R) - V(SA) mod 16 + acks = (rcvd_pdu.nr - self.send_ack) % 16 + if acks: + self.acks_recvd += acks + self.acks_ready.notify_all() + self.send_token.notify() + self.send_ack = rcvd_pdu.nr # V(SA) := N(R) + if rcvd_pdu.name == "RNR": + self.mode.SEND_BUSY = True + if rcvd_pdu.name == "RR": + self.mode.SEND_BUSY = False + + if rcvd_pdu.name == "I": + with self.lock: + # V(R) := V(R) + 1 mod 16 + self.recv_cnt = (self.recv_cnt + 1) % 16 + super(DataLinkConnection, self).enqueue(rcvd_pdu) + + def dequeue(self, miu_size, icv_size): + with self.lock: + if self.state.ESTABLISHED: + if self.mode.RECV_BUSY_SENT != self.mode.RECV_BUSY: + self.mode.RECV_BUSY_SENT = self.mode.RECV_BUSY + ACK = RNR_PDU if self.mode.RECV_BUSY else RR_PDU + return ACK(self.peer, self.addr, self.recv_ack) + + send_pdu = super(DataLinkConnection, self).dequeue( + miu_size, icv_size, notify=False) + + if send_pdu: + self.log("dequeue {0} PDU".format(send_pdu.name)) + + if send_pdu.name == "FRMR": + self.state.SHUTDOWN = True + self.close() + + if send_pdu.name == "I" and self.state.ESTABLISHED: + if self.recv_confs and self.recv_cnt != self.recv_ack: + self.log("piggyback ack " + str(self)) + self.recv_ack = (self.recv_ack + self.recv_confs) % 16 + self.recv_confs = 0 + send_pdu.nr = self.recv_ack + self.send_ready.notify() + + if send_pdu.name == "DM" and self.state.CLOSE_WAIT: + self.recv_queue.append(pdu.Disconnect( + dsap=self.peer, ssap=self.addr)) + self.recv_ready.notify() + self.send_token.notify_all() + + else: + if ((self.state.ESTABLISHED and self.recv_confs + and self.recv_window_slots == 0)): + # must send acknowledgement to keep going + self.log("necessary ack " + str(self)) + self.recv_ack = (self.recv_ack + self.recv_confs) % 16 + self.recv_confs = 0 + ACK = RNR_PDU if self.mode.RECV_BUSY else RR_PDU + return ACK(self.peer, self.addr, self.recv_ack) + + return send_pdu + + def sendack(self): + if self.state.ESTABLISHED: + with self.lock: + if self.recv_confs and self.recv_cnt != self.recv_ack: + self.log("voluntary ack " + str(self)) + self.recv_ack = (self.recv_ack + self.recv_confs) % 16 + self.recv_confs = 0 + ACK = RNR_PDU if self.mode.RECV_BUSY else RR_PDU + return ACK(self.peer, self.addr, self.recv_ack) + + +RR_PDU, RNR_PDU = pdu.ReceiveReady, pdu.ReceiveNotReady diff --git a/src/lib/nfc/snep/__init__.py b/src/lib/nfc/snep/__init__.py new file mode 100644 index 0000000..9e2145c --- /dev/null +++ b/src/lib/nfc/snep/__init__.py @@ -0,0 +1,36 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +""" +The nfc.snep module implements the NFC Forum Simple NDEF Exchange +Protocol (SNEP) specification and provides a server and client class +for applications to easily send or receive SNEP messages. +""" +from src.lib.nfc.snep.server import SnepServer # noqa: F401 +from src.lib.nfc.snep.client import SnepClient # noqa: F401 +from src.lib.nfc.snep.client import SnepError # noqa: F401 + +Success = 0x81 +NotFound = 0xC0 +ExcessData = 0xC1 +BadRequest = 0xC2 +NotImplemented = 0xE0 +UnsupportedVersion = 0xE1 diff --git a/src/lib/nfc/snep/client.py b/src/lib/nfc/snep/client.py new file mode 100644 index 0000000..73a2ade --- /dev/null +++ b/src/lib/nfc/snep/client.py @@ -0,0 +1,247 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +# +# Simple NDEF Exchange Protocol (SNEP) - Client Base Class +# +import ndef +import struct +import src.lib.nfc.llcp + +import logging +log = logging.getLogger(__name__) + + +def send_request(socket, snep_request, send_miu): + if len(snep_request) <= send_miu: + return socket.send(snep_request) + + if not socket.send(snep_request[0:send_miu]): + return False + + if socket.recv() != b"\x10\x80\x00\x00\x00\x00": + return False + + for offset in range(send_miu, len(snep_request), send_miu): + fragment = snep_request[offset:offset+send_miu] + if not socket.send(fragment): + return False + + return True + + +def recv_response(socket, acceptable_length, timeout): + if socket.poll("recv", timeout): + snep_response = socket.recv() + + if len(snep_response) < 6: + log.debug("snep response initial fragment too short") + return None + + version, status, length = struct.unpack(">BBL", snep_response[:6]) + + if length > acceptable_length: + log.debug("snep response exceeds acceptable length") + return None + + if len(snep_response) - 6 < length: + # request remaining fragments + socket.send(b"\x10\x00\x00\x00\x00\x00") + while len(snep_response) - 6 < length: + if socket.poll("recv", timeout): + snep_response += socket.recv() + else: + return None + + return bytearray(snep_response) + + +class SnepClient(object): + """ Simple NDEF exchange protocol - client implementation + """ + def __init__(self, llc, max_ndef_msg_recv_size=1024): + self.acceptable_length = max_ndef_msg_recv_size + self.socket = None + self.llc = llc + + def connect(self, service_name): + """Connect to a SNEP server. This needs only be called to + connect to a server other than the Default SNEP Server at + `urn:nfc:sn:snep` or if the client wants to send multiple + requests with a single connection. + """ + self.close() + self.socket = nfc.llcp.Socket(self.llc, nfc.llcp.DATA_LINK_CONNECTION) + self.socket.connect(service_name) + self.send_miu = self.socket.getsockopt(nfc.llcp.SO_SNDMIU) + + def close(self): + """Close the data link connection with the SNEP server. + """ + if self.socket: + self.socket.close() + self.socket = None + + def get_records(self, records=None, timeout=1.0): + """Get NDEF message records from a SNEP Server. + + .. versionadded:: 0.13 + + The :class:`ndef.Record` list given by *records* is encoded as + the request message octets input to :meth:`get_octets`. The + return value is an :class:`ndef.Record` list decoded from the + response message octets returned by :meth:`get_octets`. Same + as:: + + import ndef + send_octets = ndef.message_encoder(records) + rcvd_octets = snep_client.get_octets(send_octets, timeout) + records = list(ndef.message_decoder(rcvd_octets)) + + """ + octets = b''.join(ndef.message_encoder(records)) if records else None + octets = self.get_octets(octets, timeout) + if octets and len(octets) >= 3: + return list(ndef.message_decoder(octets)) + + def get_octets(self, octets=None, timeout=1.0): + """Get NDEF message octets from a SNEP Server. + + .. versionadded:: 0.13 + + If the client has not yet a data link connection with a SNEP + Server, it temporarily connects to the default SNEP Server, + sends the message octets, disconnects after the server + response, and returns the received message octets. + + """ + if octets is None: + # Send NDEF Message with one empty Record. + octets = b'\xd0\x00\x00' + + if not self.socket: + try: + self.connect('urn:nfc:sn:snep') + except nfc.llcp.ConnectRefused: + return None + else: + self.release_connection = True + else: + self.release_connection = False + + try: + request = struct.pack('>BBLL', 0x10, 0x01, 4 + len(octets), + self.acceptable_length) + octets + + if not send_request(self.socket, request, self.send_miu): + return None + + response = recv_response( + self.socket, self.acceptable_length, timeout) + + if response is not None: + if response[1] != 0x81: + raise SnepError(response[1]) + + return response[6:] + + finally: + if self.release_connection: + self.close() + + def put_records(self, records, timeout=1.0): + """Send NDEF message records to a SNEP Server. + + .. versionadded:: 0.13 + + The :class:`ndef.Record` list given by *records* is encoded + and then send via :meth:`put_octets`. Same as:: + + import ndef + octets = ndef.message_encoder(records) + snep_client.put_octets(octets, timeout) + + """ + octets = b''.join(ndef.message_encoder(records)) + return self.put_octets(octets, timeout) + + def put_octets(self, octets, timeout=1.0): + """Send NDEF message octets to a SNEP Server. + + .. versionadded:: 0.13 + + If the client has not yet a data link connection with a SNEP + Server, it temporarily connects to the default SNEP Server, + sends the message octets and disconnects after the server + response. + + """ + if not self.socket: + try: + self.connect('urn:nfc:sn:snep') + except nfc.llcp.ConnectRefused: + return False + else: + self.release_connection = True + else: + self.release_connection = False + + try: + request = struct.pack('>BBL', 0x10, 0x02, len(octets)) + octets + if not send_request(self.socket, request, self.send_miu): + return False + + response = recv_response(self.socket, 0, timeout) + if response is not None: + if response[1] != 0x81: + raise SnepError(response[1]) + + return True + + finally: + if self.release_connection: + self.close() + + def __enter__(self): + self.connect() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + +class SnepError(Exception): + strerr = {0xC0: "resource not found", + 0xC1: "resource exceeds data size limit", + 0xC2: "malformed request not understood", + 0xE0: "unsupported functionality requested", + 0xE1: "unsupported protocol version"} + + def __init__(self, err): + self.args = (err, SnepError.strerr.get(err, "")) + + def __str__(self): + return "nfc.snep.SnepError: [{errno}] {info}".format( + errno=self.args[0], info=self.args[1]) + + @property + def errno(self): + return self.args[0] diff --git a/src/lib/nfc/snep/server.py b/src/lib/nfc/snep/server.py new file mode 100644 index 0000000..496e437 --- /dev/null +++ b/src/lib/nfc/snep/server.py @@ -0,0 +1,175 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +# +# Simple NDEF Exchange Protocol (SNEP) - Server Base Class +# +import threading +import binascii +import logging +import struct +import errno +import ndef +import src.lib.nfc + + +log = logging.getLogger(__name__) + + +class SnepServer(threading.Thread): + """ NFC Forum Simple NDEF Exchange Protocol server + """ + def __init__(self, llc, service_name="urn:nfc:sn:snep", + max_acceptable_length=0x100000, + recv_miu=1984, recv_buf=15): + + self.max_acceptable_length = min(max_acceptable_length, 0xFFFFFFFF) + socket = nfc.llcp.Socket(llc, nfc.llcp.DATA_LINK_CONNECTION) + recv_miu = socket.setsockopt(nfc.llcp.SO_RCVMIU, recv_miu) + recv_buf = socket.setsockopt(nfc.llcp.SO_RCVBUF, recv_buf) + socket.bind(service_name) + log.info("snep server bound to port {0} (MIU={1}, RW={2}), " + "will accept up to {3} byte NDEF messages" + .format(socket.getsockname(), recv_miu, recv_buf, + self.max_acceptable_length)) + socket.listen(backlog=2) + threading.Thread.__init__(self, name=service_name, + target=self._listen, args=(socket,)) + + def _listen(self, listen_socket): + try: + while True: + client_socket = listen_socket.accept() + client_thread = threading.Thread(target=self._serve, + args=(client_socket,)) + client_thread.start() + except nfc.llcp.Error as error: + (log.debug if error.errno == errno.EPIPE else log.error)(error) + finally: + listen_socket.close() + + def _serve(self, client_socket): + peer_sap = client_socket.getpeername() + log.info("serving snep client on remote sap {0}".format(peer_sap)) + send_miu = client_socket.getsockopt(nfc.llcp.SO_SNDMIU) + try: + while client_socket.poll('recv'): + data = bytearray(client_socket.recv()) + if not data: + break # connection closed + + if len(data) < 6: + log.debug("snep msg initial fragment too short") + break # bail out, this is a bad client + + version, length = struct.unpack_from(">BxL", data) + + if (version >> 4) > 1: + log.debug("unsupported version {0}".format(version >> 4)) + client_socket.send(b"\x10\xE1\x00\x00\x00\x00") + continue + + if length > self.max_acceptable_length: + log.debug("snep msg exceeds max acceptable length") + client_socket.send(b"\x10\xFF\x00\x00\x00\x00") + continue + + if len(data) - 6 < length: + # request remaining fragments + client_socket.send(b"\x10\x80\x00\x00\x00\x00") + while len(data) - 6 < length: + try: + data += client_socket.recv() + except TypeError: + break # connection closed + + # message complete, now handle the request + data = self.process_snep_request(data) + + # send the snep response, fragment if needed + if len(data) <= send_miu: + client_socket.send(data) + else: + client_socket.send(data[0:send_miu]) + if client_socket.recv() == b"\x10\x00\x00\x00\x00\x00": + parts = range(send_miu, len(data), send_miu) + for offset in parts: + client_socket.send(data[offset:offset + send_miu]) + + except nfc.llcp.Error as e: + (log.debug if e.errno == nfc.llcp.errno.EPIPE else log.error)(e) + finally: + client_socket.close() + + def process_snep_request(self, request_data): + assert isinstance(request_data, bytearray) + log.debug("<<< %s", binascii.hexlify(request_data).decode()) + try: + if request_data[1] == 1 and len(request_data) >= 10: + acceptable_length = struct.unpack(">L", request_data[6:10])[0] + octets = request_data[10:] + records = list(ndef.message_decoder(octets, known_types={})) + response = self.process_get_request(records) + if isinstance(response, int): + response_code = response + response_data = b'' + else: + response_code = 0x81 # nfc.snep.Success + response_data = b''.join(ndef.message_encoder(response)) + if len(response_data) > acceptable_length: + response_code = 0xC1 # nfc.snep.ExcessData + response_data = b'' + elif request_data[1] == 2: + octets = request_data[6:] + records = list(ndef.message_decoder(octets, known_types={})) + response_code = self.process_put_request(records) + response_data = b'' + else: + log.debug("bad request (0x{:02x})".format(request_data[1])) + response_code = 0xC2 # nfc.snep.BadRequest + response_data = b'' + except ndef.DecodeError as error: + log.error(repr(error)) + response_code = 0xC2 # nfc.snep.BadRequest + response_data = b'' + except ndef.EncodeError as error: + log.error(repr(error)) + response_code = 0xC0 # nfc.snep.NotFound + response_data = b'' + + header = struct.pack(">BBL", 0x10, response_code, len(response_data)) + response_data = header + response_data + log.debug(">>> %s", binascii.hexlify(response_data).decode()) + return response_data + + def process_get_request(self, ndef_message): + """Handle Get requests. This method should be overwritten by a + subclass of SnepServer to customize it's behavior. The default + implementation simply returns nfc.snep.NotImplemented. + """ + return 0xE0 # NotImplemented + + def process_put_request(self, ndef_message): + """Process a SNEP Put request. This method should be overwritten by a + subclass of SnepServer to customize it's behavior. The default + implementation simply returns nfc.snep.Success. + """ + return 0x81 # nfc.snep.Success diff --git a/src/lib/nfc/tag/__init__.py b/src/lib/nfc/tag/__init__.py new file mode 100644 index 0000000..4cbfbf6 --- /dev/null +++ b/src/lib/nfc/tag/__init__.py @@ -0,0 +1,480 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2013, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import logging +from binascii import hexlify +from ndef import message_decoder, message_encoder + + +logging.captureWarnings(True) +log = logging.getLogger(__name__) + + +class Tag(object): + """The base class for all NFC Tags/Cards. The methods and attributes + defined here are commonly available but some may, depending on the + tag product, also return a :const:`None` value is support is not + available. + + Direct subclasses are the NFC Forum tag types: + :class:`~nfc.tag.tt1.Type1Tag`, :class:`~nfc.tag.tt2.Type2Tag`, + :class:`~nfc.tag.tt3.Type3Tag`, :class:`~nfc.tag.tt4.Type4Tag`. + Some of them are further specialized in vendor/product specific + classes. + + """ + class NDEF(object): + """The NDEF object type that may be read from :attr:`Tag.ndef`. + + This class presents the NDEF management information and the + actual NDEF message by a couple of attributes. It is normally + accessed from a :class:`Tag` instance (further named *tag*) + through the :attr:`Tag.ndef` attribute for reading or writing + NDEF records. :: + + if tag.ndef is not None: + for record in tag.ndef.records: + print(record) + if tag.ndef.is_writeable: + from ndef import TextRecord + tag.ndef.records = [TextRecord("Hello World")] + + """ + def __init__(self, tag): + self._tag = tag + self._data = None + self._capacity = 0 + self._readable = False + self._writeable = False + + def _read_ndef_data(self): + msg = "_read_ndef_data is not implemented for this tag type" + raise NotImplementedError(msg) + + def _write_ndef_data(self, data): + msg = "_write_ndef_data is not implemented for this tag type" + raise NotImplementedError(msg) + + @property + def tag(self): + """A readonly reference to the underlying tag object.""" + return self._tag + + @property + def length(self): + """Length of the current NDEF message in bytes.""" + return len(self._data) if self._data else 0 + + @property + def capacity(self): + """Maximum number of bytes for an NDEF message.""" + return self._capacity + + @property + def is_readable(self): + """:const:`True` if the NDEF data are is readable.""" + return self._readable + + @property + def is_writeable(self): + """:const:`True` if the NDEF data area is writeable.""" + return self._writeable + + @property + def has_changed(self): + """The boolean attribute :attr:`has_changed` allows to determine + whether the NDEF message on the tag is different from the + message that was read or written at an earlier time in the + session. This may for example be the case if the tag is + build to dynamically present different content depending + on some state. + + Note that reading this attribute involves a complete + update of the :class:`Tag.NDEF` instance and it is + possible that :attr:`Tag.ndef` is :const:`None` after the + update (e.g. tag gone during read or a dynamic tag that + failed). A robust implementation should always verify the + value of the :attr:`Tag.ndef` attribute. :: + + if tag.ndef.has_changed and tag.ndef is not None: + for record in tag.ndef.records: + print(record) + + The :attr:`has_changed` attribute can also be used to + verify that NDEF records written to the tag are identical + to the NDEF records stored on the tag. :: + + from ndef import TextRecord + tag.ndef.records = [TextRecord("Hello World")] + if tag.ndef.has_changed: + print("the tag data differs from what was written") + + """ + ndef_data = self._read_ndef_data() + different = self._data != ndef_data + if ndef_data is None: + self._tag._ndef = None + self._data = ndef_data + return different + + @property + def records(self): + """Read or write a list of NDEF Records. + + .. versionadded:: 0.12 + + This attribute is a convinience wrapper for decoding and + encoding of the NDEF message data :attr:`octets`. It uses + the `ndeflib `_ module to + return the list of :class:`ndef.Record` instances decoded + from the NDEF message data or set the message data from a + list of records. :: + + from ndef import TextRecord + if tag.ndef is not None: + for record in tag.ndef.records: + print(record) + try: + tag.ndef.records = [TextRecord('Hello World')] + except nfc.tag.TagCommandError as err: + print("NDEF write failed: " + str(err)) + + Decoding is performed with a relaxed error handling + strategy that ignores minor errors in the NDEF data. The + `ndeflib `_ does also + support 'strict' and 'ignore' error handling which may be + used like so:: + + from ndef import message_decoder, message_encoder + records = message_decoder(tag.ndef.octets, errors='strict') + tag.ndef.octets = b''.join(message_encoder(records)) + + """ + return list(message_decoder(self.octets, errors='relax')) + + @records.setter + def records(self, value): + self.octets = b''.join(message_encoder(value)) + + @property + def octets(self): + """Read or write NDEF message data octets. + + .. versionadded:: 0.12 + + The *octets* attribute returns the NDEF message data + octets as bytes. A bytes or bytearray sequence assigned to + *octets* is immediately written to the NDEF message data + area, unless the Tag memory is write protected or to + small. :: + + if tag.ndef is not None: + print(hexlify(tag.ndef.octets).decode()) + + """ + return bytes(self._data) + + @octets.setter + def octets(self, data): + if not self._writeable: + raise AttributeError("tag ndef area is not writeable") + data = bytearray(data) + if len(data) > self.capacity: + raise ValueError("data length exceeds tag capacity") + self._write_ndef_data(data) + self._data = data + + def __init__(self, clf, target): + self._clf, self._target = (clf, target) + self._ndef = None + self._authenticated = False + + def __str__(self): + """x.__str__() <==> str(x)""" + try: + s = self.type + ' ' + repr(self._product) + except AttributeError: + s = self.type + return "{} ID={}".format(s, hexlify(self.identifier).decode().upper()) + + @property + def clf(self): + return self._clf + + @property + def target(self): + return self._target + + @property + def type(self): + return self.TYPE + + @property + def product(self): + return self._product if hasattr(self, "_product") else self.type + + @property + def identifier(self): + """The unique tag identifier.""" + return bytes(self._nfcid) + + @property + def ndef(self): + """An :class:`NDEF` object if found, otherwise :const:`None`.""" + if self._ndef is None: + ndef = self.NDEF(self) + if ndef.has_changed: + self._ndef = ndef + return self._ndef + + @property + def is_present(self): + """True if the tag is within communication range.""" + return self._is_present() + + @property + def is_authenticated(self): + """True if the tag was successfully authenticated.""" + return bool(self._authenticated) + + def dump(self): + """The dump() method returns a list of strings describing the memory + structure of the tag, suitable for printing with join(). The + list format makes custom indentation a bit easier. :: + + print("\\n".join(["\\t" + line for line in tag.dump()])) + + """ + return [] + + def format(self, version=None, wipe=None): + """Format the tag to make it NDEF compatible or erase content. + + The :meth:`format` method is highly dependent on the tag type, + product and present status, for example a tag that has been + made read-only with lock bits can no longer be formatted or + erased. + + :meth:`format` creates the management information defined by + the NFC Forum to describes the NDEF data area on the tag, this + is also called NDEF mapping. The mapping may differ between + versions of the tag specifications, the mapping to apply can + be specified with the *version* argument as an 8-bit integer + composed of a major version number in the most significant 4 + bit and the minor version number in the least significant 4 + bit. If *version* is not specified then the highest possible + mapping version is used. + + If formatting of the tag is possible, the default behavior of + :meth:`format` is to update only the management information + required to make the tag appear as NDEF compatible and empty, + previously existing data could still be read. If existing data + shall be overwritten, the *wipe* argument can be set to an + 8-bit integer that will be written to all available bytes. + + The :meth:`format` method returns :const:`True` if formatting + was successful, :const:`False` if it failed for some reason, + or :const:`None` if the present tag can not be formatted + either because the tag does not support formatting or it is + not implemented in nfcpy. + + """ + if hasattr(self, "_format"): + args = "version={0!r}, wipe={1!r}" + args = args.format(version, wipe) + log.debug("format({0})".format(args)) + status = self._format(version, wipe) + if status is True: + self._ndef = None + return status + else: + log.debug("this tag can not be formatted with nfcpy") + return None + + def protect(self, password=None, read_protect=False, protect_from=0): + """Protect a tag against future write or read access. + + :meth:`protect` attempts to make a tag readonly for all + readers if *password* is :const:`None`, writeable only after + authentication if a *password* is provided, and readable only + after authentication if a *password* is provided and the + *read_protect* flag is set. The *password* must be a byte or + character sequence that provides sufficient key material for + the tag specific protect function (this is documented + separately for the individual tag types). As a special case, + if *password* is set to an empty string the :meth:`protect` + method uses a default manufacturer value if such is known. + + The *protect_from* argument sets the first memory unit to be + protected. Memory units are tag type specific, for a Type 1 or + Type 2 Tag a memory unit is 4 byte, for a Type 3 Tag it is 16 + byte, and for a Type 4 Tag it is the complete NDEF data area. + + Note that the effect of protecting a tag without password can + normally not be reversed. + + The return value of :meth:`protect` is either :const:`True` or + :const:`False` depending on whether the operation was + successful or not, or :const:`None` if the tag does not + support custom protection (or it is not implemented). + + """ + if hasattr(self, "_protect"): + args = "password={0!r}, read_protect={1!r}, protect_from={2!r}" + args = args.format(password, read_protect, protect_from) + log.debug("protect({0})".format(args)) + status = self._protect(password, read_protect, protect_from) + if status is True: + self._ndef = None + return status + else: + log.error("this tag can not be protected with nfcpy") + return None + + def authenticate(self, password): + """Authenticate a tag with a *password*. + + A tag that was once protected with a password requires + authentication before write, potentially also read, operations + may be performed. The *password* must be the same as the + password provided to :meth:`protect`. The return value + indicates authentication success with :const:`True` or + :const:`False`. For a tag that does not support authentication + the return value is :const:`None`. + + """ + if hasattr(self, "_authenticate"): + args = "password={0!r}".format(password) + log.debug("authenticate({0})".format(args)) + self._authenticated = self._authenticate(password) + if self._authenticated is True: + self._ndef = None + return self._authenticated + else: + log.error("this tag can not be authenticated with nfcpy") + return None + + +TIMEOUT_ERROR = 0 +RECEIVE_ERROR = -1 +PROTOCOL_ERROR = -2 + + +class TagCommandError(Exception): + """The base class for exceptions that are raised when a tag command + has not returned the expected result or a a lower stack error was + raised. + + The :attr:`errno` attribute holds a reason code for why the + command has failed. Error numbers greater than zero indicate a tag + type specific error from one of the exception classes derived from + :exc:`TagCommandError` (per tag type module). Error numbers below + and including zero indicate general errors:: + + nfc.tag.TIMEOUT_ERROR => unrecoverable timeout error + nfc.tag.RECEIVE_ERROR => unrecoverable transmission error + nfc.tag.PROTOCOL_ERROR => unrecoverable protocol error + + The :exc:`TagCommandError` exception populates the *message* + attribute of the general exception class with the appropriate + error description. + + """ + errno_str = { + TIMEOUT_ERROR: "unrecoverable timeout error", + RECEIVE_ERROR: "unrecoverable transmission error", + PROTOCOL_ERROR: "unrecoverable protocol error", + } + + def __init__(self, errno): + default = "tag command error {errno} (0x{errno:x})".format(errno=errno) + if errno > 0: + message = self.errno_str.get(errno, default) + else: + message = TagCommandError.errno_str.get(errno, default) + super(TagCommandError, self).__init__(message) + self._errno = errno + + @property + def errno(self): + """Holds the error reason code.""" + return self._errno + + def __int__(self): + return self._errno + + +def activate(clf, target): + import nfc.clf + try: + log.debug("trying to activate {0}".format(target)) + if target.brty.endswith('A'): + if target.sens_res[1] & 0x0F == 0x0C: + return activate_tt1(clf, target) + elif target.sel_res[0] >> 5 & 3 == 0: + return activate_tt2(clf, target) + elif target.sel_res[0] >> 5 & 1 == 1: + return activate_tt4(clf, target) + elif target.brty.endswith('B'): + return activate_tt4(clf, target) + elif target.brty.endswith('F'): + return activate_tt3(clf, target) + except nfc.clf.CommunicationError: + return None + + +def activate_tt1(clf, target): + log.debug("trying type 1 tag activation for {0}".format(target.brty)) + import nfc.tag.tt1 + return nfc.tag.tt1.activate(clf, target) + + +def activate_tt2(clf, target): + log.debug("trying type 2 tag activation for {0}".format(target.brty)) + import nfc.tag.tt2 + return nfc.tag.tt2.activate(clf, target) + + +def activate_tt3(clf, target): + log.debug("trying type 3 tag activation for {0}".format(target.brty)) + import nfc.tag.tt3 + return nfc.tag.tt3.activate(clf, target) + + +def activate_tt4(clf, target): + log.debug("trying type 4 tag activation for {0}".format(target.brty)) + import nfc.tag.tt4 + return nfc.tag.tt4.activate(clf, target) + + +class TagEmulation(object): + """Base class for tag emulation classes.""" + pass + + +def emulate(clf, target): + import nfc.clf + assert isinstance(target, nfc.clf.LocalTarget) + if target.tt3_cmd: + import nfc.tag.tt3 + return nfc.tag.tt3.Type3TagEmulation(clf, target) + else: + log.debug("can't emulate with %s", target) diff --git a/src/lib/nfc/tag/tt1.py b/src/lib/nfc/tag/tt1.py new file mode 100644 index 0000000..3e77996 --- /dev/null +++ b/src/lib/nfc/tag/tt1.py @@ -0,0 +1,555 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2011, 2017 +# Stephen Tiedemann +# Alexander Knaub +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import time +from binascii import hexlify +from struct import pack, unpack + +from . import Tag, TagCommandError +import nfc.clf + +import logging +log = logging.getLogger(__name__) + + +CHECKSUM_ERROR, RESPONSE_ERROR, WRITE_ERROR, \ + BLOCK_ERROR, SECTOR_ERROR = range(1, 6) + + +class Type1TagCommandError(TagCommandError): + """Type 1 Tag specific exceptions. Sets + :attr:`~nfc.tag.TagCommandError.errno` to one of: + + | 1 - CHECKSUM_ERROR + | 2 - RESPONSE_ERROR + | 3 - WRITE_ERROR + + """ + errno_str = { + CHECKSUM_ERROR: "crc validation failed", + RESPONSE_ERROR: "invalid response data", + WRITE_ERROR: "data write failure", + } + + +def read_tlv(memory, offset, skip_bytes): + # Unpack a TLV from tag memory and return tag type, tag length and + # tag value. For tag type 0 there is no length field, this is + # returned as length -1. The tlv length field can be one or three + # bytes, if the first byte is 255 then the next two byte carry the + # length (big endian). + try: + tlv_t, offset = (memory[offset], offset+1) + except Type1TagCommandError: + return (None, None, None) + + if tlv_t in (0x00, 0xFE): + return (tlv_t, -1, None) + + tlv_l, offset = (memory[offset], offset+1) + + if tlv_l == 0xFF: + tlv_l, offset = (unpack(">H", memory[offset:offset+2])[0], offset+2) + + tlv_v = bytearray(tlv_l) + for i in range(tlv_l): + while (offset + i) in skip_bytes: + offset += 1 + tlv_v[i] = memory[offset+i] + + return (tlv_t, tlv_l, tlv_v) + + +def get_lock_byte_range(data): + # Extract the lock byte range indicated by a Lock Control TLV. The + # data argument is the TLV value field. + page_addr = data[0] >> 4 + byte_offs = data[0] & 0x0F + rsvd_size = ((data[1] if data[1] > 0 else 256) + 7) // 8 + page_size = 2 ** (data[2] & 0x0F) + rsvd_from = page_addr * page_size + byte_offs + return slice(rsvd_from, rsvd_from + rsvd_size) + + +def get_rsvd_byte_range(data): + # Extract the reserved memory range indicated by a Memory Control + # TLV. The data argument is the TLV value field. + page_addr = data[0] >> 4 + byte_offs = data[0] & 0x0F + rsvd_size = data[1] if data[1] > 0 else 256 + page_size = 2 ** (data[2] & 0x0F) + rsvd_from = page_addr * page_size + byte_offs + return slice(rsvd_from, rsvd_from + rsvd_size) + + +def get_capacity(tag_memory_size, offset, skip_bytes): + # The net capacity is the range of bytes from the current offset + # until the end of user data bytes (given by the capability + # container capacity value plus 16 header bytes), reduced by the + # number of skip bytes (from memory and lock control TLVs) that + # are within the usable memory range, and adjusted by the required + # number of TLV length bytes (1 or 3) and the TLV tag byte. + log.debug("subtract {0} skip bytes from capacity".format(len(skip_bytes))) + capacity = len(set(range(offset, tag_memory_size)) - skip_bytes) + # To store more than 254 byte ndef we must use three length bytes, + # otherwise it's only one. But only if the capacity is more than + # 256 the three length byte format will provide a higher value. + capacity -= 4 if capacity > 256 else 2 + return capacity + + +class Type1Tag(Tag): + """Implementation of the NFC Forum Type 1 Tag Operation specification. + + The NFC Forum Type 1 Tag is based on the ISO 14443 Type A + technology for frame structure and anticollision (detection) + commands, and the Innovision (now Broadcom) Jewel/Topaz commands + for accessing the tag memory. + + """ + TYPE = "Type1Tag" + + class NDEF(Tag.NDEF): + # Type 1 Tag specific implementation of the NDEF access type + # class that is returned by the Tag.ndef attribute. + + def __init__(self, tag): + super(Type1Tag.NDEF, self).__init__(tag) + self._ndef_tlv_offset = 0 + + def _read_ndef_data(self): + # Check and read ndef data from tag. Return None if the + # tag is not ndef formatted, i.e. it can not hold ndef + # data or does not have (valid) ndef management data. + # Otherwise, set state variables and return the ndef + # message data as a bytearray (may be zero length). + log.debug("read ndef data") + try: + tag_memory = Type1TagMemoryReader(self.tag) + + if tag_memory._header_rom[0] >> 4 != 1: + log.debug("proprietary type 1 tag memory structure") + return None + + if tag_memory[8] != 0xE1: + log.debug("ndef management data is not present") + return None + + if tag_memory[9] >> 4 != 1: + log.debug("unsupported ndef mapping version") + return None + + self._readable = bool(tag_memory[11] >> 4 == 0) + self._writeable = bool(tag_memory[11] & 0xF == 0) + + tag_memory_size = (tag_memory[10] + 1) * 8 + log.debug("tag memory size is %d byte" % tag_memory_size) + except Type1TagCommandError: + log.debug("header rom and static memory were unreadable") + return None + + ndef = None + offset = 12 + skip_end = 120 if tag_memory_size == 120 else 128 + skip_bytes = set(range(104, skip_end)) + while offset < tag_memory_size: + if offset in skip_bytes: + offset += 1 + continue + + tlv_t, tlv_l, tlv_v = read_tlv(tag_memory, offset, skip_bytes) + log.debug("tlv type {0} at address {1}".format(tlv_t, offset)) + + if tlv_t == 0x00: + pass + elif tlv_t == 0x01: + lock_bytes = get_lock_byte_range(tlv_v) + skip_bytes.update(range(*lock_bytes.indices(0x800))) + elif tlv_t == 0x02: + rsvd_bytes = get_rsvd_byte_range(tlv_v) + skip_bytes.update(range(*rsvd_bytes.indices(0x800))) + elif tlv_t == 0x03: + ndef = tlv_v + break + elif tlv_t == 0xFE or tlv_t is None: + break + else: + logmsg = "unknown tlv {0} at offset {0}" + log.debug(logmsg.format(tlv_t, offset)) + + offset += tlv_l + 1 + (1 if tlv_l < 255 else 3) + + self._capacity = get_capacity(tag_memory_size, offset, skip_bytes) + self._ndef_tlv_offset = offset + self._tag_memory = tag_memory + self._skip_bytes = skip_bytes + return ndef + + def _write_ndef_data(self, data): + log.debug("write ndef data {0}{1}".format( + hexlify(data[:10]).decode(), '...' if len(data) > 10 else '')) + + tag_memory = self._tag_memory + skip_bytes = self._skip_bytes + offset = self._ndef_tlv_offset + tag_memory_size = (tag_memory[10] + 1) * 8 + + # Set the ndef message tlv length to 0. + tag_memory[offset+1] = 0 + tag_memory.synchronize() + + # Leave room for ndef message length byte(s) and write + # ndef data into the memory image, but jump over skip + # bytes. + offset += 2 if len(data) < 255 else 4 + for i in range(len(data)): + while offset + i in skip_bytes: + offset += 1 + tag_memory[offset+i] = data[i] + # Write a terminator tlv if space permits. We may have to + # skip reserved and lock bytes. + offset = offset + i + 1 + while offset < tag_memory_size: + if offset not in skip_bytes: + tag_memory[offset] = 0xFE + break + offset += 1 + # Write the new message data to the tag. + tag_memory.synchronize() + + # Write the ndef message tlv length. + offset = self._ndef_tlv_offset + if len(data) < 255: + tag_memory[offset+1] = len(data) + else: + tag_memory[offset+1] = 0xFF + tag_memory[offset+2:offset+4] = pack(">H", len(data)) + tag_memory.synchronize() + + # + # Type1Tag methods and attributes + # + def __init__(self, clf, target): + super(Type1Tag, self).__init__(clf, target) + self._nfcid = self.uid = target.rid_res[2:6] + + def dump(self): + """Returns the tag memory blocks as a list of formatted strings. + + :meth:`dump` iterates over all tag memory blocks (8 bytes + each) from block zero until the physical end of memory and + produces a list of strings that is intended for line by line + printing. Multiple consecutive memory block of identical + content may be reduced to fewer lines of output, so the number + of lines returned does not necessarily correspond to the + number of memory blocks present. + + .. warning:: For tags with more than 120 byte memory, the + dump() method first overwrites the data block to verify + that it is backed by physical memory, then restores the + original data. This is necessary because Type 1 Tags do + not indicate an error when reading beyond the physical + memory space. Be cautious to not remove a tag from the + reader when using dump() as otherwise your data may be + corrupted. + + """ + return self._dump(stop=None) + + def _dump(self, stop=None): + # Read and print all data blocks until the non-inclusive stop + # block number. Type 1 Tags with dynamic memory seem to return + # data for every address, regardless of whether there is + # memory mapped or not. To show exactly the memory blocks that + # are physically present, blocks from 16-end are first + # overwritten with an inverted version of the content and then + # recovered. Because WRITE8 returns the new data content, a + # non-existing block can be detected. + + def oprint(octets): + return ' '.join(['??' if x < 0 else '%02x' % x for x in octets]) + + def cprint(octets): + return ''.join([chr(x) if 32 <= x <= 126 else '.' for x in octets]) + + def lprint(fmt, d, i): + return fmt.format(i, oprint(d), cprint(d)) + + txt = ["UID0-UID6, RESERVED", "RESERVED", "LOCK0-LOCK1, OTP0-OTP5", + "LOCK2-LOCK3, RESERVED"] + + lines = list() + data = self.read_all() + hrom, data = data[0:2], data[2:] + + lines.append("HR0={0:02X}h, HR1={1:02X}h".format(*hrom)) + lines.append(" 0: {0} ({1})".format(oprint(data[0:8]), txt[0])) + for i in range(8, 104, 8): + lines.append(lprint("{0:3}: {1} |{2}|", data[i:i+8], i//8)) + lines.append(" 13: {0} ({1})".format(oprint(data[104:112]), txt[1])) + lines.append(" 14: {0} ({1})".format(oprint(data[112:120]), txt[2])) + + if stop is None or stop > 15: + try: + data = self.read_block(15) + except Type1TagCommandError: + return lines + else: + lines.append(" 15: {0} ({1})".format(oprint(data), txt[3])) + + data_line_fmt = "{0:>3}: {1} |{2}|" + same_line_fmt = "{0:>3} {1} |{2}|" + this_data = last_data = None + same_data = 0 + + def dump_same_data(same_data, last_data, this_data, page): + if same_data > 1: + lines.append(lprint(same_line_fmt, last_data, "*")) + if same_data > 0: + lines.append(lprint(data_line_fmt, this_data, page)) + + for i in range(16, stop if stop is not None else 256): + try: + this_data = self.read_block(i) + if stop is None: + test_data = bytearray([b ^ 0xFF for b in this_data]) + self.write_block(i, test_data) + self.write_block(i, this_data) + except Type1TagCommandError: + dump_same_data(same_data, last_data, this_data, i-1) + break + + if this_data == last_data: + same_data += 1 + else: + dump_same_data(same_data, last_data, last_data, i-1) + lines.append(lprint(data_line_fmt, this_data, i)) + last_data = this_data + same_data = 0 + else: + dump_same_data(same_data, last_data, this_data, i) + + return lines + + def protect(self, password=None, read_protect=False, protect_from=0): + """The implementation of :meth:`nfc.tag.Tag.protect` for a generic + type 1 tag is limited to setting the NDEF data read-only for + tags that are already NDEF formatted. + + """ + return super(Type1Tag, self).protect( + password, read_protect, protect_from) + + def _protect(self, password, read_protect, protect_from): + if password is None: + if self.ndef is not None: + self.write_byte(11, 0x0F, erase=False) + return True + else: + log.warning("no ndef, can't set write access restriction") + else: + log.warning("this tag can not be protected with a password") + return False + + def _is_present(self): + try: + return self.read_byte(0) == self.uid[0] + except Type1TagCommandError: + return False + + def read_id(self): + """Returns the 2 byte Header ROM and 4 byte UID. + """ + log.debug("read identification") + cmd = b"\x78\x00\x00\x00\x00\x00\x00" + return self.transceive(cmd) + + def read_all(self): + """Returns the 2 byte Header ROM and all 120 byte static memory. + """ + log.debug("read all static memory") + cmd = b"\x00\x00\x00" + self.uid + return self.transceive(cmd) + + def read_byte(self, addr): + """Read a single byte from static memory area (blocks 0-14). + """ + if addr < 0 or addr > 127: + raise ValueError("invalid byte address") + log.debug("read byte at address {0} ({0:02X}h)".format(addr)) + cmd = bytearray([0x01, addr, 0x00]) + self.uid + return self.transceive(cmd)[-1] + + def read_block(self, block): + """Read an 8-byte data block at address (block * 8). + """ + if block < 0 or block > 255: + raise ValueError("invalid block number") + log.debug("read block {0}".format(block)) + cmd = bytearray([0x02, block] + [0x00 for _ in range(8)]) + self.uid + return self.transceive(cmd)[1:9] + + def read_segment(self, segment): + """Read one memory segment (128 byte). + """ + log.debug("read segment {0}".format(segment)) + if segment < 0 or segment > 15: + raise ValueError("invalid segment number") + cmd = bytearray([0x10, segment << 4] + [0x00 for _ in range(8)]) \ + + self.uid + rsp = self.transceive(cmd) + if len(rsp) < 129: + raise Type1TagCommandError(RESPONSE_ERROR) + return rsp[1:129] + + def write_byte(self, addr, data, erase=True): + """Write a single byte to static memory area (blocks 0-14). The + target byte is zero'd first if *erase* is True. + + """ + if addr < 0 or addr >= 128: + raise ValueError("invalid byte address") + log.debug("write byte at address {0} ({0:02X}h)".format(addr)) + cmd = b"\x53" if erase is True else b"\x1A" + cmd = cmd + bytearray([addr, data]) + self.uid + return self.transceive(cmd) + + def write_block(self, block, data, erase=True): + """Write an 8-byte data block at address (block * 8). The target + bytes are zero'd first if *erase* is True. + + """ + if block < 0 or block > 255: + raise ValueError("invalid block number") + log.debug("write block {0}".format(block)) + cmd = b"\x54" if erase is True else b"\x1B" + cmd = cmd + bytearray([block]) + data + self.uid + rsp = self.transceive(cmd) + if len(rsp) < 9: + raise Type1TagCommandError(RESPONSE_ERROR) + if erase is True and rsp[1:9] != data: + raise Type1TagCommandError(WRITE_ERROR) + + def transceive(self, data, timeout=0.1): + log.debug(">> {0} ({1:f}s)".format(hexlify(data).decode(), timeout)) + + started = time.time() + error = None + for retry in range(3): + try: + data = self.clf.exchange(data, timeout) + break + except nfc.clf.CommunicationError as e: + error = e + reason = error.__class__.__name__ + log.debug("%s after %d retries" % (reason, retry)) + else: + if type(error) is nfc.clf.TimeoutError: + raise Type1TagCommandError(nfc.tag.TIMEOUT_ERROR) + if type(error) is nfc.clf.TransmissionError: + raise Type1TagCommandError(nfc.tag.RECEIVE_ERROR) + if type(error) is nfc.clf.ProtocolError: + raise Type1TagCommandError(nfc.tag.PROTOCOL_ERROR) + raise RuntimeError("unexpected " + repr(error)) + + elapsed = time.time() - started + log.debug("<< {0} ({1:f}s)".format(hexlify(data).decode(), elapsed)) + return data + + +class Type1TagMemoryReader(object): + def __init__(self, tag): + assert isinstance(tag, Type1Tag) + self._data_from_tag = bytearray() + self._data_in_cache = bytearray() + self._tag = tag + self._header_rom = bytearray(0) + # read header_rom and static memory + self._read_from_tag(1) + + def __len__(self): + return len(self._data_from_tag) + + def __getitem__(self, key): + if isinstance(key, slice): + start, stop, step = key.indices(0x100000) + if stop > len(self): + self._read_from_tag(stop) + elif key >= len(self): + self._read_from_tag(stop=key+1) + return self._data_in_cache[key] + + def __setitem__(self, key, value): + self.__getitem__(key) + if isinstance(key, slice): + if len(value) != len(range(*key.indices(0x100000))): + msg = "{cls} requires item assignment of identical length" + raise ValueError(msg.format(cls=self.__class__.__name__)) + self._data_in_cache[key] = value + del self._data_in_cache[len(self):] + + def __delitem__(self, key): + msg = "{cls} object does not support item deletion" + raise TypeError(msg.format(cls=self.__class__.__name__)) + + def _read_from_tag(self, stop): + if len(self) < 120: + read_all_data_response = self._tag.read_all() + self._header_rom = read_all_data_response[0:2] + self._data_from_tag[0:] = read_all_data_response[2:] + self._data_in_cache[0:] = self._data_from_tag[0:] + + if stop > 120 and len(self) < 128: + read_block_response = self._tag.read_block(15) + self._data_from_tag[120:128] = read_block_response + self._data_in_cache[120:128] = read_block_response + + while len(self) < stop: + data = self._tag.read_segment(len(self) >> 7) + self._data_from_tag.extend(data) + self._data_in_cache.extend(data) + + def _write_to_tag(self, stop): + hr0 = self._header_rom[0] + if hr0 >> 4 == 1 and hr0 & 0x0F != 1: + for i in range(0, stop, 8): + data = self._data_in_cache[i:i+8] + if data != self._data_from_tag[i:i+8]: + self._tag.write_block(i//8, data) + self._data_from_tag[i:i+8] = data + else: + for i in range(0, stop): + data = self._data_in_cache[i] + if data != self._data_from_tag[i]: + self._tag.write_byte(i, data) + self._data_from_tag[i] = data + + def synchronize(self): + """Write pages that contain modified data back to tag memory.""" + self._write_to_tag(stop=len(self)) + + +def activate(clf, target): + import nfc.tag.tt1_broadcom + tag = nfc.tag.tt1_broadcom.activate(clf, target) + return tag if tag is not None else Type1Tag(clf, target) diff --git a/src/lib/nfc/tag/tt1_broadcom.py b/src/lib/nfc/tag/tt1_broadcom.py new file mode 100644 index 0000000..7f700fe --- /dev/null +++ b/src/lib/nfc/tag/tt1_broadcom.py @@ -0,0 +1,159 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2014, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +from . import tt1 + +import logging +log = logging.getLogger(__name__) + + +class Topaz(tt1.Type1Tag): + """The Broadcom Topaz is a small memory tag that can hold up to 94 + byte ndef message data. + + """ + def __init__(self, clf, target): + super(Topaz, self).__init__(clf, target) + self._product = "Topaz (BCM20203T96)" + + def dump(self): + return super(Topaz, self)._dump(stop=15) + + def format(self, version=None, wipe=None): + """Format a Topaz tag for NDEF use. + + The implementation of :meth:`nfc.tag.Tag.format` for a Topaz + tag creates a capability container and an NDEF TLV with length + zero. Data bytes of the NDEF data area are left untouched + unless the wipe argument is set. + + """ + return super(Topaz, self).format(version, wipe) + + def _format(self, version, wipe): + tag_memory = tt1.Type1TagMemoryReader(self) + tag_memory[8:14] = b"\xE1\x10\x0E\x00\x03\x00" + + if version is not None: + if version >> 4 == 1: + tag_memory[9] = version + else: + log.warning("can not format with major version != 1") + return False + + if wipe is not None: + tag_memory[14:104] = bytearray([wipe & 0xFF]) * 90 + + tag_memory.synchronize() + return True + + def protect(self, password=None, read_protect=False, protect_from=0): + """In addtion to :meth:`nfc.tag.tt1.Type1Tag.protect` this method + tries to set the lock bits to irreversibly protect the tag + memory. However, it appears that tags sold have the lock bytes + write protected, so this additional effort most likely doesn't + have any effect. + + """ + return super(Topaz, self).protect( + password, read_protect, protect_from) + + def _protect(self, password, read_protect, protect_from): + if super(Topaz, self)._protect(password, read_protect, protect_from): + self.write_byte(112, 0xFF, erase=False) + self.write_byte(113, 0xFF, erase=False) + return True + else: + return False + + +class Topaz512(tt1.Type1Tag): + """The Broadcom Topaz-512 is a memory enhanced version that can hold + up to 462 byte ndef message data. + + """ + def __init__(self, clf, target): + super(Topaz512, self).__init__(clf, target) + self._product = "Topaz 512 (BCM20203T512)" + + def dump(self): + return super(Topaz512, self)._dump(stop=64) + + def format(self, version=None, wipe=None): + """Format a Topaz-512 tag for NDEF use. + + The implementation of :meth:`nfc.tag.Tag.format` for a + Topaz-512 tag creates a capability container, a Lock Control + and a Memory Control TLV, and an NDEF TLV with length + zero. Data bytes of the NDEF data area are left untouched + unless the wipe argument is set. + + """ + return super(Topaz512, self).format(version, wipe) + + def _format(self, version, wipe): + tag_memory = tt1.Type1TagMemoryReader(self) + tag_memory[8:16] = bytearray.fromhex("E1103F000103F230") + tag_memory[16:24] = bytearray.fromhex("330203F002030300") + + if version is not None: + if version >> 4 == 1: + tag_memory[9] = version + else: + log.warning("can not format with major version != 1") + return False + + if wipe is not None: + tag_memory[24:104] = bytearray([wipe & 0xFF]) * 80 + tag_memory[128:512] = bytearray([wipe & 0xFF]) * 384 + + tag_memory.synchronize() + return True + + def protect(self, password=None, read_protect=False, protect_from=0): + """In addtion to :meth:`nfc.tag.tt1.Type1Tag.protect` this method + tries to set the lock bits to irreversibly protect the tag + memory. However, it appears that tags sold have the lock bytes + write protected, so this additional effort most likely doesn't + have any effect. + + """ + return super(Topaz512, self).protect( + password, read_protect, protect_from) + + def _protect(self, password, read_protect, protect_from): + if super(Topaz512, self)._protect( + password, read_protect, protect_from): + self.write_byte(112, 0xFF, erase=False) + self.write_byte(113, 0xFF, erase=False) + self.write_byte(120, 0xFF, erase=False) + self.write_byte(121, 0xFF, erase=False) + return True + else: + return False + + +def activate(clf, target): + hrom = target.rid_res[0:2] + if hrom == b"\x11\x48": + return Topaz(clf, target) + if hrom == b"\x12\x4C": + return Topaz512(clf, target) diff --git a/src/lib/nfc/tag/tt2.py b/src/lib/nfc/tag/tt2.py new file mode 100644 index 0000000..f26120c --- /dev/null +++ b/src/lib/nfc/tag/tt2.py @@ -0,0 +1,697 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# Licensed under the EUPL, Version 1.1 or - as soon they +# You may obtain a copy of the Licence at: +# + +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import time +from binascii import hexlify +from struct import pack, unpack + +from . import Tag, TagCommandError +import nfc.clf + +import logging +log = logging.getLogger(__name__) + + +def hexdump(octets, sep=""): + return sep.join( + ("??" if x is None else ("%02x" % x)) for x in octets) + + +def chrdump(octets, sep=""): + return sep.join( + (("{:c}".format(x) if 32 <= x <= 126 else ".") + if x is not None + else ".") + for x in octets) + + +def pagedump(page, octets, info=None): + info = ("|%s|" % chrdump(octets)) if info is None else ("(%s)" % info) + page = " * " if page is None else "{0:03X}:".format(page) + return "{0} {1} {2}".format(page, hexdump(octets, sep=" "), info) + + +TIMEOUT_ERROR, INVALID_SECTOR_ERROR, \ + INVALID_PAGE_ERROR, INVALID_RESPONSE_ERROR = range(4) + + +class Type2TagCommandError(TagCommandError): + """Type 2 Tag specific exceptions. Sets + :attr:`~nfc.tag.TagCommandError.errno` to one of: + + | 1 - INVALID_SECTOR_ERROR + | 2 - INVALID_PAGE_ERROR + | 3 - INVALID_RESPONSE_ERROR + + """ + errno_str = { + INVALID_SECTOR_ERROR: "invalid sector number", + INVALID_PAGE_ERROR: "invalid page number", + INVALID_RESPONSE_ERROR: "invalid response data", + } + + +def read_tlv(memory, offset, skip_bytes): + # Unpack a Type 2 Tag TLV from tag memory and return tag type, tag + # length and tag value. For tag type 0 there is no length field, + # this is returned as length -1. The tlv length field can be one + # or three bytes, if the first byte is 255 then the next two byte + # carry the length (big endian). + tlv_t, offset = (memory[offset], offset+1) + if tlv_t in (0x00, 0xFE): + return (tlv_t, -1, None) + tlv_l, offset = (memory[offset], offset+1) + if tlv_l == 0xFF: + tlv_l, offset = (unpack(">H", memory[offset:offset+2])[0], offset+2) + tlv_v = bytearray(tlv_l) + for i in range(tlv_l): + while (offset + i) in skip_bytes: + offset += 1 + tlv_v[i] = memory[offset+i] + return (tlv_t, tlv_l, tlv_v) + + +def get_lock_byte_range(data): + # Extract the lock byte range indicated by a Lock Control TLV. The + # data argument is the TLV value field. + page_addr = data[0] >> 4 + byte_offs = data[0] & 0x0F + rsvd_size = ((data[1] if data[1] > 0 else 256) + 7) // 8 + page_size = 2 ** (data[2] & 0x0F) + rsvd_from = page_addr * page_size + byte_offs + return slice(rsvd_from, rsvd_from + rsvd_size) + + +def get_rsvd_byte_range(data): + # Extract the reserved memory range indicated by a Memory Control + # TLV. The data argument is the TLV value field. + page_addr = data[0] >> 4 + byte_offs = data[0] & 0x0F + rsvd_size = data[1] if data[1] > 0 else 256 + page_size = 2 ** (data[2] & 0x0F) + rsvd_from = page_addr * page_size + byte_offs + return slice(rsvd_from, rsvd_from + rsvd_size) + + +def get_capacity(capacity, offset, skip_bytes): + # The net capacity is the range of bytes from the current offset + # until the end of user data bytes (given by the capability + # container capacity value plus 16 header bytes), reduced by the + # number of skip bytes (from memory and lock control TLVs) that + # are within the usable memory range, and adjusted by the required + # number of TLV length bytes (1 or 3) and the TLV tag byte. + capacity = len(set(range(offset, capacity + 16)) - skip_bytes) + # To store more than 254 byte ndef we must use three length bytes, + # otherwise it's only one. But only if the capacity is more than + # 256 the three length byte format will provide a higher value. + capacity -= 4 if capacity > 256 else 2 + return capacity + + +class Type2Tag(Tag): + """Implementation of the NFC Forum Type 2 Tag Operation specification. + + The NFC Forum Type 2 Tag is based on the ISO 14443 Type A + technology for frame structure and anticollision (detection) + commands, and the NXP Mifare commands for accessing the tag + memory. + + """ + TYPE = "Type2Tag" + + class NDEF(Tag.NDEF): + # Type 2 Tag specific implementation of the NDEF access type + # class that is returned by the Tag.ndef attribute. + + def __init__(self, tag): + super(Type2Tag.NDEF, self).__init__(tag) + self._ndef_tlv_offset = 0 + + def _read_capability_data(self, tag_memory): + try: + if tag_memory[12] != 0xE1: + log.debug("ndef management data is not present") + return False + if tag_memory[13] >> 4 != 1: + log.debug("unsupported ndef mapping major version") + return False + self._readable = bool(tag_memory[15] >> 4 == 0) + self._writeable = bool(tag_memory[15] & 0xF == 0) + return True + except Type2TagCommandError: + log.debug("first four memory pages were unreadable") + return False + + def _read_ndef_data(self): + log.debug("read ndef data") + tag_memory = Type2TagMemoryReader(self.tag) + + if not self._read_capability_data(tag_memory): + return None + + raw_capacity = tag_memory[14] * 8 + log.debug("raw capacity is {0} byte".format(raw_capacity)) + + offset = 16 + ndef = None + skip_bytes = set() + data_area_size = raw_capacity + while offset < data_area_size + 16: + while (offset) in skip_bytes: + offset += 1 + + try: + tlv = read_tlv(tag_memory, offset, skip_bytes) + tlv_t, tlv_l, tlv_v = tlv + except Type2TagCommandError: + return None + else: + logmsg = "tlv type {0} length {1} at offset {2}" + log.debug(logmsg.format(tlv_t, tlv_l, offset)) + + if tlv_t == 0: + pass + elif tlv_t == 1: + if tlv_l == 3: + lock_bytes = get_lock_byte_range(tlv_v) + skip_bytes.update(range(*lock_bytes.indices(0x100000))) + else: + log.debug("lock tlv has wrong length") + elif tlv_t == 2: + if tlv_l == 3: + rsvd_bytes = get_rsvd_byte_range(tlv_v) + skip_bytes.update(range(*rsvd_bytes.indices(0x100000))) + else: + log.debug("memory tlv has wrong length") + elif tlv_t == 3: + ndef = tlv_v + break + elif tlv_t == 254: + break + else: + logmsg = "unknown tlv {0} at offset {0}" + log.debug(logmsg.format(tlv_t, offset)) + + offset += tlv_l + 1 + (1 if tlv_l < 255 else 3) + + self._capacity = get_capacity(raw_capacity, offset, skip_bytes) + self._ndef_tlv_offset = offset + self._tag_memory = tag_memory + self._skip_bytes = skip_bytes + return ndef + + def _write_ndef_data(self, data): + # Write new ndef data to the tag memory. Despite the + # tag memory is rather easy to handle, the extremely + # generic NFC Forum TLV structure makes this rather + # complicated. The precondition is that we have already + # processed the memory structure in _read_ndef_data(), if + # not we'll do it first. We'll then have a tag memory + # image, know which bytes need to be to skipped as told by + # memory or control tlv data, and where the ndef message + # tlv starts. We first set the ndef message tlv length to + # zero (synchronize cause that to be actually written), + # then write all new data into the memory image (skipping + # bytes as needed) and let that be written to the tag, and + # finally write the new ndef message tlv length. + log.debug("write ndef data {0}{1}".format( + hexlify(data[:10]).decode(), '...' if len(data) > 10 else '')) + + tag_memory = self._tag_memory + skip_bytes = self._skip_bytes + offset = self._ndef_tlv_offset + + # Set the ndef message tlv length to 0. + tag_memory[offset+1] = 0 + tag_memory.synchronize() + + # Leave room for ndef message length byte(s) and write + # ndef data into the memory image, but jump over skip + # bytes. If space permits, write a terminator tlv. + offset += 2 if len(data) < 255 else 4 + for index, octet in enumerate(data): + while offset + index in skip_bytes: + offset += 1 + tag_memory[offset+index] = octet + offset = offset + index + 1 + while offset in skip_bytes: + offset += 1 + if offset < tag_memory[14] * 8 + 16: + tag_memory[offset] = 0xFE + tag_memory.synchronize() + + # Write the ndef message tlv length. + offset = self._ndef_tlv_offset + if len(data) < 255: + tag_memory[offset+1] = len(data) + else: + tag_memory[offset+1] = 0xFF + tag_memory[offset+2:offset+4] = pack(">H", len(data)) + tag_memory.synchronize() + + # + # Type2Tag methods and attributes + # + def __init__(self, clf, target): + super(Type2Tag, self).__init__(clf, target) + self._nfcid = bytearray(target.sdd_res) + self._current_sector = 0 + + def dump(self): + """Returns the tag memory pages as a list of formatted strings. + + :meth:`dump` iterates over all tag memory pages (4 bytes + each) from page zero until an error response is received and + produces a list of strings that is intended for line by line + printing. Note that multiple consecutive memory pages of + identical content may be reduced to fewer lines of output, so + the number of lines returned does not necessarily correspond + to the number of memory pages. + + """ + return self._dump(stop=None) + + def _dump(self, stop=None): + lines = list() + header = ("UID0-UID2, BCC0", "UID3-UID6", + "BCC1, INT, LOCK0-LOCK1", "OTP0-OTP3") + + for i, info in enumerate(header): + try: + data = self.read(i)[0:4] + except Type2TagCommandError: + data = [None, None, None, None] + lines.append(pagedump(i, data, info)) + + this_data = last_data = None + same_data = 0 + + def dump_same_data(same_data, last_data, this_data, page): + if same_data > 1: + lines.append(pagedump(None, this_data)) + if same_data > 0: + lines.append(pagedump(page, this_data)) + + for i in range(4, stop if stop is not None else 0x40000): + try: + self.sector_select(i >> 8) + this_data = self.read(i)[0:4] + except Type2TagCommandError: + dump_same_data(same_data, last_data, this_data, i-1) + if stop is not None: + this_data = last_data = [None, None, None, None] + lines.append(pagedump(i, this_data)) + dump_same_data(stop-i-1, this_data, this_data, stop-1) + break + + if this_data == last_data: + same_data += 1 + else: + dump_same_data(same_data, last_data, last_data, i-1) + lines.append(pagedump(i, this_data)) + last_data = this_data + same_data = 0 + else: + dump_same_data(same_data, last_data, this_data, i) + + return lines + + def _is_present(self): + # Verify that the tag is still present. This is implemented as + # reading page 0-3 (from whatever sector is currently active). + try: + data = self.transceive(b"\x30\x00") + except Type2TagCommandError as error: + if error.errno != TIMEOUT_ERROR: + log.warning("unexpected error in presence check: %s" % error) + return False + else: + return bool(data and len(data) == 16) + + def format(self, version=None, wipe=None): + """Erase the NDEF message on a Type 2 Tag. + + The :meth:`format` method will reset the length of the NDEF + message on a type 2 tag to zero, thus the tag will appear to + be empty. Additionally, if the *wipe* argument is set to some + integer then :meth:`format` will overwrite all user date that + follows the NDEF message TLV with that integer (mod 256). If + an NDEF message TLV is not present it will be created with a + length of zero. + + Despite it's name, the :meth:`format` method can not format a + blank tag to make it NDEF compatible. This is because the user + data are of a type 2 tag can not be safely determined, also + reading all memory pages until an error response yields only + the total memory size which includes an undetermined number of + special pages at the end of memory. + + It is also not possible to change the NDEF mapping version, + located in a one-time-programmable area of the tag memory. + + """ + return super(Type2Tag, self).format(version, wipe) + + def _format(self, version, wipe): + if self.ndef and self.ndef.is_writeable: + memory = self.ndef._tag_memory + offset = self.ndef._ndef_tlv_offset + memory[offset+1:offset+3] = b"\x00\xFE" + if wipe is not None: + memory_size = memory[14] * 8 + 16 + skip_bytes = self.ndef._skip_bytes + for offset in range(offset + 3, memory_size): + if offset not in skip_bytes: + memory[offset] = wipe & 0xFF + memory.synchronize() + return True + return False + + def protect(self, password=None, read_protect=False, protect_from=0): + """Protect the tag against write access, i.e. make it read-only. + + :meth:`Type2Tag.protect` switches an NFC Forum Type 2 Tag to + read-only state by setting all lock bits to 1. This operation + can not be reversed. If the tag is not an NFC Forum Tag, + i.e. it is not formatted with an NDEF Capability Container, + the :meth:`protect` method simply returns :const:`False`. + + A generic Type 2 Tag can not be protected with a password. If + the *password* argument is provided, the :meth:`protect` + method does nothing else than return :const:`False`. The + *read_protect* and *protect_from* arguments are safely + ignored. + + """ + return super(Type2Tag, self).protect( + password, read_protect, protect_from) + + def _protect(self, password, read_protect, protect_from): + if password is not None: + log.debug("this tag can not be protected with password") + return False + + if self.ndef is None: + log.debug("can not protect a non-ndef tag") + return False + + # Set the ndef capability container write flag. We must + # synchronize to have this written before lock bits are set. + tag_memory = self.ndef._tag_memory + tag_memory[15] |= 0x0F + tag_memory.synchronize() + + # Set the static lock bits. + tag_memory[10] = 0xFF + tag_memory[11] = 0xFF + + # Search for all lock control tlv and store the first lock + # byte address and the number of lock bits in lock_control. + offset = 16 + lock_control = [] + data_area_size = tag_memory[14] * 8 + while offset < data_area_size + 16: # pragma: no branch + tlv_t, tlv_l, tlv_v = read_tlv(tag_memory, offset, set()) + log.debug("tlv type {0} at offset {1}".format(tlv_t, offset)) + if tlv_t in (0x03, 0xFE, None): + break + if tlv_t == 0x01: + log.debug("lock control tlv %s", hexlify(tlv_v).decode()) + page_addr = tlv_v[0] >> 4 + byte_offs = tlv_v[0] & 0x0F + page_size = 2 ** (tlv_v[2] & 0x0F) # BytesPerPage + lock_byte_addr = page_addr * page_size + byte_offs + lock_bits_size = tlv_v[1] if tlv_v[1] > 0 else 256 + lock_control.append((lock_byte_addr, lock_bits_size)) + offset += tlv_l + 1 + (1 if tlv_l < 255 else 3) + + # If the tag has a dynamic memory layout and we did not find + # any lock control tlv, then add default dynamic lock bits. + if tag_memory[14] > 6 and len(lock_control) == 0: + # use default dynamic lock bits layout + data_area_size = tag_memory[14] * 8 + lock_byte_addr = 16 + data_area_size + lock_bits_size = (data_area_size - 48 + 7)//8 + lock_control.append((lock_byte_addr, lock_bits_size)) + + # For any lock control entry set the referenced lock bytes to + # zero and then set the lock bits to one. + log.debug("processing lock byte list {0}".format(lock_control)) + for lock_byte_addr, lock_bits_size in lock_control: + log.debug("{0} lock bits at 0x{1:02x}".format( + lock_bits_size, lock_byte_addr)) + lock_byte_size = (lock_bits_size + 7) // 8 + for i in range(lock_byte_size): + tag_memory[lock_byte_addr+i] = 0 + for i in range(lock_bits_size): + tag_memory[lock_byte_addr+(i >> 3)] |= 1 << (i & 7) + + # Synchronize to write all lock bits to the tag. + tag_memory.synchronize() + return True + + def read(self, page): + """Send a READ command to retrieve data from the tag. + + The *page* argument specifies the offset in multiples of 4 + bytes (i.e. page number 1 will return bytes 4 to 19). The data + returned is a byte array of length 16 or None if the block is + outside the readable memory range. + + Command execution errors raise :exc:`Type2TagCommandError`. + + """ + log.debug("read pages {0} to {1}".format(page, page+3)) + + data = self.transceive(bytearray([0x30, page % 256]), timeout=0.005) + + if len(data) == 1 and data[0] & 0xFA == 0x00: + log.debug("received nak response") + self.target.sel_req = self.target.sdd_res[:] + self._target = self.clf.sense(self.target) + raise Type2TagCommandError( + INVALID_PAGE_ERROR if self.target else nfc.tag.RECEIVE_ERROR) + + if len(data) != 16: + log.debug("invalid response %s", hexlify(data).decode()) + raise Type2TagCommandError(INVALID_RESPONSE_ERROR) + + return data + + def write(self, page, data): + """Send a WRITE command to store data on the tag. + + The *page* argument specifies the offset in multiples of 4 + bytes. The *data* argument must be a string or bytearray of + length 4. + + Command execution errors raise :exc:`Type2TagCommandError`. + + """ + if len(data) != 4: + raise ValueError("data must be a four byte string or array") + + log.debug("write %s to page %s", hexlify(data).decode(), page) + rsp = self.transceive(bytearray([0xA2, page % 256]) + data) + + if len(rsp) != 1: + log.debug("invalid response %s", hexlify(data).decode()) + raise Type2TagCommandError(INVALID_RESPONSE_ERROR) + + if rsp[0] != 0x0A: # NAK + log.debug("invalid page, received nak") + raise Type2TagCommandError(INVALID_PAGE_ERROR) + + return True + + def sector_select(self, sector): + """Send a SECTOR_SELECT command to switch the 1K address sector. + + The command is only send to the tag if the *sector* number is + different from the currently selected sector number (set to 0 + when the tag instance is created). If the command was + successful, the currently selected sector number is updated + and further :meth:`read` and :meth:`write` commands will be + relative to that sector. + + Command execution errors raise :exc:`Type2TagCommandError`. + + """ + if sector != self._current_sector: + log.debug("select sector {0} (pages {1} to {2})".format( + sector, sector << 10, ((sector+1) << 8) - 1)) + + sector_select_1 = b'\xC2\xFF' + sector_select_2 = pack('Bxxx', sector) + + rsp = self.transceive(sector_select_1) + if len(rsp) == 1 and rsp[0] == 0x0A: + try: + # command is passively ack'd, i.e. there's no response + # and we must make sure there's no retries attempted + self.transceive(sector_select_2, timeout=0.001, retries=0) + except Type2TagCommandError as error: + assert int(error) == TIMEOUT_ERROR # passive ack + else: + log.debug("sector {0} does not exist".format(sector)) + raise Type2TagCommandError(INVALID_SECTOR_ERROR) + else: + log.debug("sector select is not supported for this tag") + raise Type2TagCommandError(INVALID_SECTOR_ERROR) + + log.debug("sector {0} is now selected".format(sector)) + self._current_sector = sector + return self._current_sector + + def transceive(self, data, timeout=0.1, retries=2): + """Send a Type 2 Tag command and receive the response. + + :meth:`transceive` is a type 2 tag specific wrapper around the + :meth:`nfc.ContactlessFrontend.exchange` method. It can be + used to send custom commands as a sequence of *data* bytes to + the tag and receive the response data bytes. If *timeout* + seconds pass without a response, the operation is aborted and + :exc:`~nfc.tag.TagCommandError` raised with the TIMEOUT_ERROR + error code. + + Command execution errors raise :exc:`Type2TagCommandError`. + + """ + log.debug(">> {0} ({1:f}s)".format(hexlify(data).decode(), timeout)) + + if not self.target: + # Sometimes we have to (re)sense the target during + # communication. If that failed (tag gone) then any + # further attempt to transceive() is the same as + # "unrecoverable timeout error". + raise Type2TagCommandError(nfc.tag.TIMEOUT_ERROR) + + started = time.time() + error = None + for retry in range(1 + retries): + try: + data = self.clf.exchange(data, timeout) + break + except nfc.clf.CommunicationError as e: + error = e + reason = error.__class__.__name__ + log.debug("%s after %d retries" % (reason, retry)) + else: + if type(error) is nfc.clf.TimeoutError: + raise Type2TagCommandError(nfc.tag.TIMEOUT_ERROR) + if type(error) is nfc.clf.TransmissionError: + raise Type2TagCommandError(nfc.tag.RECEIVE_ERROR) + if type(error) is nfc.clf.ProtocolError: + raise Type2TagCommandError(nfc.tag.PROTOCOL_ERROR) + raise RuntimeError("unexpected " + repr(error)) + + elapsed = time.time() - started + log.debug("<< {0} ({1:f}s)".format(hexlify(data).decode(), elapsed)) + return data + + +class Type2TagMemoryReader(object): + """The memory reader provides a convenient way to read and write + :class:`Type2Tag` memory. Once instantiated with a proper type + 2 *tag* object the tag memory can then be accessed as a linear + sequence of bytes, without any considerations of sector or + page boundaries. Modified bytes can be written to tag memory + with :meth:`synchronize`. :: + + clf = nfc.ContactlessFrontend(...) + tag = clf.connect(rdwr={'on-connect': None}) + if isinstance(tag, nfc.tag.tt2.Type2Tag): + tag_memory = nfc.tag.tt2.Type2TagMemoryReader(tag) + tag_memory[16:19] = [0x03, 0x00, 0xFE] + tag_memory.synchronize() + + """ + def __init__(self, tag): + assert isinstance(tag, Type2Tag) + self._data_from_tag = bytearray() + self._data_in_cache = bytearray() + self._tag = tag + + def __len__(self): + return len(self._data_from_tag) + + def __getitem__(self, key): + if isinstance(key, slice): + start, stop, step = key.indices(0x100000) + if stop > len(self): + self._read_from_tag(stop) + elif key >= len(self): + self._read_from_tag(stop=key+1) + return self._data_in_cache[key] + + def __setitem__(self, key, value): + self.__getitem__(key) + if isinstance(key, slice): + if len(value) != len(range(*key.indices(0x100000))): + msg = "{cls} requires item assignment of identical length" + raise ValueError(msg.format(cls=self.__class__.__name__)) + self._data_in_cache[key] = value + del self._data_in_cache[len(self):] + + def __delitem__(self, key): + msg = "{cls} object does not support item deletion" + raise TypeError(msg.format(cls=self.__class__.__name__)) + + def _read_from_tag(self, stop): + index = (len(self) >> 4) << 4 + while index < stop: + self._tag.sector_select(index >> 10) + data = self._tag.read(index >> 2) + self._data_from_tag[index:] = data + self._data_in_cache[index:] = data + index += 16 + + def _write_to_tag(self, stop): + index = 0 + while index < stop: + data = self._data_in_cache[index:index+4] + if data != self._data_from_tag[index:index+4]: + self._tag.sector_select(index >> 10) + self._tag.write(index >> 2, data) + self._data_from_tag[index:index+4] = data + index += 4 + + def synchronize(self): + """Write pages that contain modified data back to tag memory.""" + self._write_to_tag(stop=len(self)) + + +def activate(clf, target): + # Type 2 Tags go mute when they receive an unsupported command. It + # is then necessary to sense again and by copying sdd_res to + # sel_req we ensure that only the same tag will be found. + target.sel_req = target.sdd_res[:] + if target.sdd_res[0] == 0x04: # NXP + import nfc.tag.tt2_nxp + tag = nfc.tag.tt2_nxp.activate(clf, target) + if tag is not None: + return tag + else: + # make sure the tag is still alive + target = clf.sense(target) + if target: + return Type2Tag(clf, target) diff --git a/src/lib/nfc/tag/tt2_nxp.py b/src/lib/nfc/tag/tt2_nxp.py new file mode 100644 index 0000000..1089990 --- /dev/null +++ b/src/lib/nfc/tag/tt2_nxp.py @@ -0,0 +1,771 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2014, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import nfc.clf +from . import tt2 + +import os +import struct +from binascii import hexlify +from pyDes import triple_des, CBC + +import logging +log = logging.getLogger(__name__) + + +class MifareUltralight(tt2.Type2Tag): + """Mifare Ultralight is a simple type 2 tag with no specific + features. It can store up to 46 byte NDEF message data. This class + does not do much more than to provide the known memory size. + + """ + def __init__(self, clf, target): + super(MifareUltralight, self).__init__(clf, target) + self._product = "Mifare Ultralight (MF01CU1)" + + def dump(self): + return super(MifareUltralight, self)._dump(stop=16) + + +class MifareUltralightC(tt2.Type2Tag): + """Mifare Ultralight C provides more memory, to store up to 142 byte + NDEF message data, and can be password protected. + + """ + class NDEF(tt2.Type2Tag.NDEF): + def _read_capability_data(self, tag_memory): + base_class = super(MifareUltralightC.NDEF, self) + if base_class._read_capability_data(tag_memory): + if self.tag.is_authenticated: + if not self._readable and tag_memory[15] >> 4 == 8: + self._readable = True + if not self._writeable and tag_memory[15] & 0xF == 8: + self._writeable = bool(tag_memory[10:12] == b"\0\0") + return True + return False + + def __init__(self, clf, target): + super(MifareUltralightC, self).__init__(clf, target) + self._product = "Mifare Ultralight C (MF01CU2)" + + def dump(self): + lines = super(MifareUltralightC, self)._dump(stop=40) + + footer = dict(zip(range(40, 44), ( + "LOCK2-LOCK3", "CTR0-CTR1", "AUTH0", "AUTH1"))) + + for i in sorted(footer.keys()): + try: + data = self.read(i)[0:4] + except tt2.Type2TagCommandError: + data = [None, None, None, None] + lines.append(tt2.pagedump(i, data, footer[i])) + + return lines + + def protect(self, password=None, read_protect=False, protect_from=0): + """Protect a Mifare Ultralight C Tag. + + A Mifare Ultrlight C Tag can be provisioned with a custom + password (or the default manufacturer key if the password is + an empty string or bytearray). + + A non-empty *password* must provide at least 128 bit key + material, in other words it must be a string or bytearray of + length 16 or more. + + If *password* is not None, the first protected memory page can + be specified with the *protect_from* integer argument. A + memory page is 4 byte and the total number of pages is 48. A + *protect_from* argument of 48 effectively disables memory + protection. A *protect_from* argument of 3 protects all user + data pages including the bitwise one-time-programmable page + 3. Any value less than 3 or more than 48 is accepted but to + the same effect as if 3 or 48 were specified. If effective + protection starts at page 3 and the tag is formatted for NDEF, + the :meth:`protect` method does also modify the NDEF + read/write capability byte. + + If *password* is not None and *read_protect* is True then the + tag memory content will also be protected against read access, + i.e. successful authentication will be required to read + protected pages. + + The :meth:`protect` method verifies a password change by + authenticating with the new *password* after all modifications + were made and returns the result of :meth:`authenticate`. + + .. warning:: If protect is called without a password, the + default Type 2 Tag protection method will set the lock + bits to readonly. This process is not reversible. + + """ + args = (password, read_protect, protect_from) + return super(MifareUltralightC, self).protect(*args) + + def _protect(self, password, read_protect, protect_from): + if password is None: + return self._protect_with_lockbits() + else: + args = (password, read_protect, protect_from) + return self._protect_with_password(*args) + + def _protect_with_lockbits(self): + try: + ndef_cc = self.read(3)[0:4] + if ndef_cc[0] == 0xE1 and ndef_cc[1] >> 4 == 1: + ndef_cc[3] = 0x0F + self.write(3, ndef_cc) + self.write(2, b"\x00\x00\xFF\xFF") + self.write(40, b"\xFF\xFF\x00\x00") + return True + except tt2.Type2TagCommandError: + return False + + def _protect_with_password(self, password, read_protect, protect_from): + if password and len(password) < 16: + raise ValueError("password must be at least 16 byte") + + # The first 16 password character bytes are taken as key + # unless the password is empty. If it's empty we use the + # factory default password. + key = password[0:16] if password != b"" else b"IEMKAERB!NACUOYF" + log.debug("protect with key %s", hexlify(key).decode()) + + # split the key and reverse + key1, key2 = key[7::-1], key[15:7:-1] + self.write(44, key1[0:4]) + self.write(45, key1[4:8]) + self.write(46, key2[0:4]) + self.write(47, key2[4:8]) + + # protect from memory page + self.write(42, bytearray([max(3, min(protect_from, 0x30))]) + + b"\0\0\0") + + # set read protection flag + self.write(43, b"\0\0\0\0" if read_protect else b"\x01\0\0\0") + + # Set NDEF read/write permissions if protection starts at page + # 3 and the tag is formatted for NDEF. We set the read/write + # permission flags to 8, thus indicating proprietary access. + if protect_from <= 3: + ndef_cc = self.read(3)[0:4] + if ndef_cc[0] == 0xE1 and ndef_cc[1] & 0xF0 == 0x10: + ndef_cc[3] |= (0x88 if read_protect else 0x08) + self.write(3, ndef_cc) + + # Reactivate the tag to have the key effective and + # authenticate with the same key + self._target = self.clf.sense(self.target) + return self.authenticate(key) if self.target else False + + def authenticate(self, password): + """Authenticate with a Mifare Ultralight C Tag. + + :meth:`autenticate` executes the Mifare Ultralight C mutual + authentication protocol to verify that the *password* argument + matches the key that is stored in the card. A new card key can + be set with :meth:`protect`. + + The *password* argument must be a string with either 0 or at + least 16 bytes. A zero length password string indicates that + the factory default card key be used. From a password with 16 + or more bytes the first 16 byte are taken as card key, + remaining bytes are ignored. A password length between 1 and + 15 generates a ValueError exception. + + The authentication result is True if the password was + confirmed and False if not. + + """ + return super(MifareUltralightC, self).authenticate(password) + + def _authenticate(self, password): + # The first 16 password character bytes are taken as key + # unless the password is empty. If it's empty we use the + # factory default password. + key = password[0:16] if password != b"" else b"IEMKAERB!NACUOYF" + + if len(key) != 16: + raise ValueError("password must be at least 16 byte") + + log.debug("authenticate with key %s", hexlify(key).decode()) + + rsp = self.transceive(b"\x1A\x00") + m1 = bytes(rsp[1:9]) + iv = b"\x00\x00\x00\x00\x00\x00\x00\x00" + rb = triple_des(key, CBC, iv).decrypt(m1) + + log.debug("received challenge") + log.debug("iv = %s", hexlify(iv).decode()) + log.debug("m1 = %s", hexlify(m1).decode()) + log.debug("rb = %s", hexlify(rb).decode()) + + ra = os.urandom(8) + iv = bytes(rsp[1:9]) + + m2 = triple_des(key, CBC, iv).encrypt(ra + rb[1:8] + ( + struct.pack("B", rb[0]) if isinstance(rb[0], int) else rb[0])) + + log.debug("sending response") + log.debug("ra = %s", hexlify(ra).decode()) + log.debug("iv = %s", hexlify(iv).decode()) + log.debug("m2 = %s", hexlify(m2).decode()) + try: + rsp = self.transceive(b"\xAF" + m2) + except tt2.Type2TagCommandError: + return False + + m3 = bytes(rsp[1:9]) + iv = m2[8:16] + log.debug("received confirmation") + log.debug("iv = %s", hexlify(iv).decode()) + log.debug("m3 = %s", hexlify(m3).decode()) + + return triple_des(key, CBC, iv).decrypt(m3) == ra[1:9] \ + + (struct.pack("B", ra[0]) if isinstance(ra[0], int) else ra[0]) + + +class NTAG203(tt2.Type2Tag): + """The NTAG203 is a plain memory Tag with 144 bytes user data memory + plus a 16-bit one-way counter. It does not have any security + features beyond the standard lock bit mechanism that permanently + disables write access. + + """ + def __init__(self, clf, target): + super(NTAG203, self).__init__(clf, target) + self._product = "NXP NTAG203" + + def dump(self): + lines = super(NTAG203, self)._dump(40) + + footer = dict(zip(range(40, 42), ("LOCK2-LOCK3", "CNTR0-CNTR1"))) + + for i in sorted(footer.keys()): + try: + data = self.read(i)[0:4] + except tt2.Type2TagCommandError: + data = [None, None, None, None] + lines.append(tt2.pagedump(i, data, footer[i])) + + return lines + + def protect(self, password=None, read_protect=False, protect_from=0): + """Set lock bits to disable future memory modifications. + + If *password* is None, all memory pages except the 16-bit + counter in page 41 are protected by setting the relevant lock + bits (note that lock bits can not be reset). If valid NDEF + management data is found in page 4, protect() also sets the + NDEF write flag to read-only. + + The NTAG203 can not be password protected. If a *password* + argument is provided, the protect() method always returns + False. + + """ + return super(NTAG203, self).protect( + password, read_protect, protect_from) + + def _protect(self, password, read_protect, protect_from): + if password is None: + try: + ndef_cc = self.read(3)[0:4] + if ndef_cc[0] == 0xE1 and ndef_cc[1] >> 4 == 1: + ndef_cc[3] = 0x0F + self.write(3, ndef_cc) + self.write(2, b"\x00\x00\xFF\xFF") + self.write(40, b"\xFF\x01\x00\x00") + return True + except tt2.Type2TagCommandError: + pass + return False + + def _format(self, version, wipe): + if self.ndef is None: + log.debug("no management data, writing factory defaults") + self.write(4, b'\x01\x03\xA0\x10') + self.write(5, b'\x44\x03\x00\xFE') + return super(NTAG203, self)._format(version, wipe) + + +class NTAG21x(tt2.Type2Tag): + """Base class for the NTAG21x family (210/212/213/215/216). The + methods and attributes documented here are supported for all + NTAG21x products. + + All NTAG21x products support a simple password protection scheme + that can be configured to restrict write as well as read access to + memory starting from a selected page address. A factory programmed + ECC signature allows to verify the tag unique identifier. + + """ + class NDEF(tt2.Type2Tag.NDEF): + def _read_capability_data(self, tag_memory): + if super(NTAG21x.NDEF, self)._read_capability_data(tag_memory): + if self.tag.is_authenticated: + if not self._readable and tag_memory[15] >> 4 == 8: + self._readable = True + if not self._writeable and tag_memory[15] & 0xF == 8: + self._writeable = bool(tag_memory[10:12] == b"\0\0") + return True + return False + + @property + def signature(self): + """The 32-byte ECC tag signature programmed at chip production. The + signature is provided as a string and can only be read. + + The signature attribute is always loaded from the tag when it + is accessed, i.e. it is not cached. If communication with the + tag fails for some reason the signature attribute is set to a + 32-byte string of all zeros. + + """ + log.debug("read tag signature") + try: + return bytes(self.transceive(b"\x3C\x00")) + except tt2.Type2TagCommandError: + return 32 * b"\0" + + def protect(self, password=None, read_protect=False, protect_from=0): + """Set password protection or permanent lock bits. + + If the *password* argument is None, all memory pages will be + protected by setting the relevant lock bits (note that lock + bits can not be reset). If valid NDEF management data is + found, protect() also sets the NDEF write flag to read-only. + + All Tags of the NTAG21x family can alternatively be protected + by password. If a *password* argument is provided, the + protect() method writes the first 4 byte of the *password* + string into the Tag's password (PWD) memory bytes and the + following 2 byte of the *password* string into the password + acknowledge (PACK) memory bytes. Factory default values are + used if the *password* argument is an empty string. Lock bits + are not set for password protection. + + The *read_protect* and *protect_from* arguments are only + evaluated if *password* is not None. If *read_protect* is + True, the memory protection bit (PROT) is set to require + password verification also for reading of protected memory + pages. The value of *protect_from* determines the first + password protected memory page (one page is 4 byte) with the + exception that the smallest set value is page 3 even if + *protect_from* is smaller. + + """ + args = (password, read_protect, protect_from) + return super(NTAG21x, self).protect(*args) + + def _protect(self, password, read_protect, protect_from): + if password is None: + return self._protect_with_lockbits() + else: + args = (password, read_protect, protect_from) + return self._protect_with_password(*args) + + def _protect_with_lockbits(self): + try: + ndef_cc = self.read(3)[0:4] + if ndef_cc[0] == 0xE1 and ndef_cc[1] >> 4 == 1: + ndef_cc[3] = 0x0F + self.write(3, ndef_cc) + self.write(2, b"\x00\x00\xFF\xFF") + if self._cfgpage > 16: + self.write(self._cfgpage - 1, b"\xFF\xFF\xFF\x00") + cfgdata = self.read(self._cfgpage) + if cfgdata[4] & 0x40 == 0: + cfgdata[4] |= 0x40 # set CFGLCK bit + self.write(self._cfgpage + 1, cfgdata[4:8]) + return True + except tt2.Type2TagCommandError: + return False + + def _protect_with_password(self, password, read_protect, protect_from): + if password and len(password) < 6: + raise ValueError("password must be at least 6 bytes") + + key = password[0:6] if password != b"" else b"\xFF\xFF\xFF\xFF\0\0" + log.debug("protect with key %s", hexlify(key).decode()) + + # read CFG0, CFG1, PWD and PACK + cfg = self.read(self._cfgpage) + + # set password and acknowledge + cfg[8:14] = key + + # start protection from page + cfg[3] = max(3, min(protect_from, 255)) + + # set read protection bit + cfg[4] = cfg[4] | 0x80 if read_protect else cfg[4] & 0x7F + + # write configuration to tag + for i in range(4): + self.write(self._cfgpage + i, cfg[i*4:(i+1)*4]) + + # Set NDEF read/write permissions if protection starts at page + # 3 and the tag is formatted for NDEF. We set the read/write + # permission flags to 8, thus indicating proprietary access. + if protect_from <= 3: + ndef_cc = self.read(3)[0:4] + if ndef_cc[0] == 0xE1 and ndef_cc[1] & 0xF0 == 0x10: + ndef_cc[3] |= (0x88 if read_protect else 0x08) + self.write(3, ndef_cc) + + # Reactivate the tag to have the key effective and + # authenticate with the same key + self._target = self.clf.sense(self.target) + return self.authenticate(key) if self.target else False + + def authenticate(self, password): + """Authenticate with password to access protected memory. + + An NTAG21x implements a simple password protection scheme. The + reader proofs possession of a share secret by sending a 4-byte + password and the tag proofs possession of a shared secret by + returning a 2-byte password acknowledge. Because password and + password acknowledge are transmitted in plain text special + considerations should be given to under which conditions + authentication is performed. If, for example, an attacker is + able to mount a relay attack both secret values are easily + lost. + + The *password* argument must be a string of length zero or at + least 6 byte characters. If the *password* length is zero, + authentication is performed with factory default values. If + the *password* contains at least 6 bytes, the first 4 byte are + send to the tag as the password secret and the following 2 + byte are compared against the password acknowledge that is + received from the tag. + + The authentication result is True if the password was + confirmed and False if not. + + """ + return super(NTAG21x, self).authenticate(password) + + def _authenticate(self, password): + if password and len(password) < 6: + raise ValueError("password must be at least 6 bytes") + + key = password[0:6] if password != b"" else b"\xFF\xFF\xFF\xFF\0\0" + log.debug("authenticate with key %s", hexlify(key).decode()) + + try: + rsp = self.transceive(b"\x1B" + key[0:4]) + return rsp == key[4:6] + except tt2.Type2TagCommandError: + return False + + def _dump(self, stop, footer): + lines = super(NTAG21x, self)._dump(stop) + for i in sorted(footer.keys()): + try: + data = self.read(i)[0:4] + except tt2.Type2TagCommandError: + data = [None, None, None, None] + lines.append(tt2.pagedump(i, data, footer[i])) + return lines + + +class NTAG210(NTAG21x): + """The NTAG210 provides 48 bytes user data memory, password + protection, originality signature and a UID mirror function. + + """ + def __init__(self, clf, target): + super(NTAG210, self).__init__(clf, target) + self._product = "NXP NTAG210" + self._cfgpage = 16 + + def _format(self, version, wipe): + if self.ndef is None: + log.debug("no management data, writing factory defaults") + self.write(4, b'\x03\x00\xFE\x00') + self.write(5, b'\x00\x00\x00\x00') + return super(NTAG210, self)._format(version, wipe) + + def dump(self): + footer = dict(zip(range(16, 20), + ("MIRROR_BYTE, RFU, MIRROR_PAGE, AUTH0", + "ACCESS", "PWD0-PWD3", "PACK0-PACK1"))) + return super(NTAG210, self)._dump(16, footer) + + +class NTAG212(NTAG21x): + """The NTAG212 provides 128 bytes user data memory, password + protection, originality signature and a UID mirror function. + + """ + def __init__(self, clf, target): + super(NTAG212, self).__init__(clf, target) + self._product = "NXP NTAG212" + self._cfgpage = 37 + + def _format(self, version, wipe): + if self.ndef is None: + log.debug("no management data, writing factory defaults") + self.write(4, b'\x01\x03\x90\x0A') + self.write(5, b'\x34\x03\x00\xFE') + return super(NTAG212, self)._format(version, wipe) + + def dump(self): + text = ("LOCK2-LOCK4", "MIRROR_BYTE, RFU, MIRROR_PAGE, AUTH0", + "ACCESS", "PWD0-PWD3", "PACK0-PACK1") + footer = dict(zip(range(36, 36+len(text)), text)) + return super(NTAG212, self)._dump(36, footer) + + +class NTAG213(NTAG21x): + """The NTAG213 provides 144 bytes user data memory, password + protection, originality signature, a tag read counter and a mirror + function for the tag unique identifier and the read counter. + + """ + def __init__(self, clf, target): + super(NTAG213, self).__init__(clf, target) + self._product = "NXP NTAG213" + self._cfgpage = 41 + + def _format(self, version, wipe): + if self.ndef is None: + log.debug("no management data, writing factory defaults") + self.write(4, b'\x01\x03\xA0\x0C') + self.write(5, b'\x34\x03\x00\xFE') + return super(NTAG213, self)._format(version, wipe) + + def dump(self): + text = ("LOCK2-LOCK4", "MIRROR, RFU, MIRROR_PAGE, AUTH0", + "ACCESS", "PWD0-PWD3", "PACK0-PACK1") + footer = dict(zip(range(40, 40+len(text)), text)) + return super(NTAG213, self)._dump(40, footer) + + +class NTAG215(NTAG21x): + """The NTAG215 provides 504 bytes user data memory, password + protection, originality signature, a tag read counter and a mirror + function for the tag unique identifier and the read counter. + + """ + def __init__(self, clf, target): + super(NTAG215, self).__init__(clf, target) + self._product = "NXP NTAG215" + self._cfgpage = 131 + + def _format(self, version, wipe): + if self.ndef is None: + log.debug("no management data, writing factory defaults") + self.write(4, b'\x03\x00\xFE\x00') + self.write(5, b'\x00\x00\x00\x00') + return super(NTAG215, self)._format(version, wipe) + + def dump(self): + text = ("LOCK2-LOCK4", "MIRROR, RFU, MIRROR_PAGE, AUTH0", + "ACCESS", "PWD0-PWD3", "PACK0-PACK1") + footer = dict(zip(range(130, 130+len(text)), text)) + return super(NTAG215, self)._dump(130, footer) + + +class NTAG216(NTAG21x): + """The NTAG216 provides 888 bytes user data memory, password + protection, originality signature, a tag read counter and a mirror + function for the tag unique identifier and the read counter. + + """ + def __init__(self, clf, target): + super(NTAG216, self).__init__(clf, target) + self._product = "NXP NTAG216" + self._cfgpage = 227 + + def _format(self, version, wipe): + if self.ndef is None: + log.debug("no management data, writing factory defaults") + self.write(4, b'\x03\x00\xFE\x00') + self.write(5, b'\x00\x00\x00\x00') + return super(NTAG216, self)._format(version, wipe) + + def dump(self): + text = ("LOCK2-LOCK4", "MIRROR, RFU, MIRROR_PAGE, AUTH0", + "ACCESS", "PWD0-PWD3", "PACK0-PACK1") + footer = dict(zip(range(226, 226+len(text)), text)) + return super(NTAG216, self)._dump(226, footer) + + +class MifareUltralightEV1(NTAG21x): + """Mifare Ultralight EV1 + + """ + def __init__(self, clf, target, product): + super(MifareUltralightEV1, self).__init__(clf, target) + self._product = "Mifare Ultralight EV1 ({0})".format(product) + + def _dump_ul11(self): + text = ("MOD, RFU, RFU, AUTH0", "ACCESS, VCTID, RFU, RFU", + "PWD0, PWD1, PWD2, PWD3", "PACK0, PACK1, RFU, RFU") + footer = dict(zip(range(16, 16+len(text)), text)) + return super(MifareUltralightEV1, self)._dump(16, footer) + + def _dump_ul21(self): + text = ("LOCK2, LOCK3, LOCK4, RFU", + "MOD, RFU, RFU, AUTH0", "ACCESS, VCTID, RFU, RFU", + "PWD0, PWD1, PWD2, PWD3", "PACK0, PACK1, RFU, RFU") + footer = dict(zip(range(36, 36+len(text)), text)) + return super(MifareUltralightEV1, self)._dump(36, footer) + + +class MF0UL11(MifareUltralightEV1): + def __init__(self, clf, target): + super(MF0UL11, self).__init__(clf, target, "MF0UL11") + + def dump(self): + return self._dump_ul11() + + +class MF0ULH11(MifareUltralightEV1): + def __init__(self, clf, target): + super(MF0ULH11, self).__init__(clf, target, "MF0ULH11") + + def dump(self): + return self._dump_ul11() + + +class MF0UL21(MifareUltralightEV1): + def __init__(self, clf, target): + super(MF0UL21, self).__init__(clf, target, "MF0UL21") + + def dump(self): + return self._dump_ul21() + + +class MF0ULH21(MifareUltralightEV1): + def __init__(self, clf, target): + super(MF0ULH21, self).__init__(clf, target, "MF0ULH21") + + def dump(self): + return self._dump_ul21() + + +class NTAGI2C(tt2.Type2Tag): + def _dump(self, stop): + s = super(NTAGI2C, self)._dump(stop) + + data = self.read(stop)[0:4] + s.append(tt2.pagedump(stop, data, "LOCK2-LOCK4, CHK")) + + data = self.read(232) + s.append("") + s.append("Configuration registers:") + s.append(tt2.pagedump(stop & 256 | 232, data[0:4], + "NC, LD, SM, WDT0")) + s.append(tt2.pagedump(stop & 256 | 233, data[4:8], + "WDT1, CLK, LOCK, RFU")) + + self.sector_select(3) + data = self.read(248) + s.append("") + s.append("Session registers:") + s.append(tt2.pagedump(0x3F8, data[0:4], "NC, LD, SM, WDT0")) + s.append(tt2.pagedump(0x3F9, data[4:8], "WDT1, CLK, NS, RFU")) + + self.sector_select(0) + return s + + +class NT3H1101(NTAGI2C): + """NTAG I2C 1K. + + """ + def __init__(self, clf, target): + super(NT3H1101, self).__init__(clf, target) + self._product = "NTAG I2C 1K (NT3H1101)" + + def dump(self): + return super(NT3H1101, self)._dump(226) + + +class NT3H1201(NTAGI2C): + """NTAG I2C 2K. + + """ + def __init__(self, clf, target): + super(NT3H1201, self).__init__(clf, target) + self._product = "NTAG I2C 2K (NT3H1201)" + + def dump(self): + return super(NT3H1201, self)._dump(480) + + +VERSION_MAP = { + b"\x00\x04\x03\x01\x01\x00\x0B\x03": MF0UL11, + b"\x00\x04\x03\x02\x01\x00\x0B\x03": MF0ULH11, + b"\x00\x04\x03\x01\x01\x00\x0E\x03": MF0UL21, + b"\x00\x04\x03\x02\x01\x00\x0E\x03": MF0ULH21, + b"\x00\x04\x04\x01\x01\x00\x0B\x03": NTAG210, + b"\x00\x04\x04\x01\x01\x00\x0E\x03": NTAG212, + b"\x00\x04\x04\x02\x01\x00\x0F\x03": NTAG213, + b"\x00\x04\x04\x02\x01\x00\x11\x03": NTAG215, + b"\x00\x04\x04\x02\x01\x00\x13\x03": NTAG216, + b"\x00\x04\x04\x05\x02\x01\x13\x03": NT3H1101, + b"\x00\x04\x04\x05\x02\x01\x15\x03": NT3H1201, + # b"\x00\x04\x04\x05\x02\x02\x13\x03": NT3H2111, + # b"\x00\x04\x04\x05\x02\x02\x15\x03": NT3H2211, +} + + +def activate(clf, target): + log.debug("check if authenticate command is available") + try: + rsp = clf.exchange(b'\x1A\x00', timeout=0.01) + if clf.sense(target) is None: + return + if rsp.startswith(b"\xAF"): + return MifareUltralightC(clf, target) + except nfc.clf.TimeoutError: + if clf.sense(target) is None: + return + except nfc.clf.CommunicationError as error: + log.debug(repr(error)) + return + + log.debug("check if version command is available") + try: + rsp = bytes(clf.exchange(b'\x60', timeout=0.01)) + if rsp in VERSION_MAP: + return VERSION_MAP[rsp](clf, target) + if rsp == b"\x00": + if clf.sense(target) is None: + return None + else: + return NTAG203(clf, target) + log.debug("no match for version %s", hexlify(rsp).decode().upper()) + return + except nfc.clf.TimeoutError: + if clf.sense(target) is None: + return + except nfc.clf.CommunicationError as error: + log.debug(repr(error)) + return + + return MifareUltralight(clf, target) diff --git a/src/lib/nfc/tag/tt3.py b/src/lib/nfc/tag/tt3.py new file mode 100644 index 0000000..d77a5f9 --- /dev/null +++ b/src/lib/nfc/tag/tt3.py @@ -0,0 +1,930 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2009, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import nfc.tag +import nfc.clf + +import math +import time +import itertools +from binascii import hexlify +from struct import pack, unpack + +import logging +log = logging.getLogger(__name__) + + +RSP_LENGTH_ERROR, RSP_CODE_ERROR, TAG_IDM_ERROR, DATA_SIZE_ERROR = range(1, 5) + + +class Type3TagCommandError(nfc.tag.TagCommandError): + errno_str = { + RSP_LENGTH_ERROR: "invalid response length", + RSP_CODE_ERROR: "invalid response code", + TAG_IDM_ERROR: "answer from wrong tag", + DATA_SIZE_ERROR: "insufficient data received", + # FeliCa Lite specific error codes + 0x01A6: "invalid service code number or attribute", + 0x01B1: "authentication required to read (first block in list)", + 0x02B1: "authentication required to read (second block in list)", + 0x04B1: "authentication required to read (third block in list)", + 0x08B1: "authentication required to read (fourth block in list)", + 0x02B2: "verification failure for write with mac operation", + } + + +class ServiceCode: + """A service code provides access to a group of data blocks located on + the card file system. A service code is a 16-bit structure + composed of a 10-bit service number and a 6-bit service + attribute. The service attribute determines the service type and + whether authentication is required. + + """ + def __init__(self, number, attribute): + self.number = number + self.attribute = attribute + + def __repr__(self): + return "ServiceCode({0}, {1})".format(self.number, self.attribute) + + def __str__(self): + attribute_map = { + 0b001000: "Random RW with key", + 0b001001: "Random RW w/o key", + 0b001010: "Random RO with key", + 0b001011: "Random RO w/o key", + 0b001100: "Cyclic RW with key", + 0b001101: "Cyclic RW w/o key", + 0b001110: "Cyclic RO with key", + 0b001111: "Cyclic RO w/o key", + 0b010000: "Purse Direct with key", + 0b010001: "Purse Direct w/o key", + 0b010010: "Purse Cashback with key", + 0b010011: "Purse Cashback w/o key", + 0b010100: "Purse Decrement with key", + 0b010101: "Purse Decrement w/o key", + 0b010110: "Purse Read Only with key", + 0b010111: "Purse Read Only w/o key", + } + try: + attribute_string = attribute_map[self.attribute] + except KeyError: + attribute_string = "Type {0:06b}b".format(self.attribute) + return "Service Code {0:04X}h (Service {1} {2!s})".format( + int(self), self.number, attribute_string) + + def __int__(self): + return self.number << 6 | self.attribute + + def pack(self): + """Pack the service code for transmission. Returns a 2 byte string.""" + sn, sa = self.number, self.attribute + return pack("> 6, v & 0x3f) + + +class BlockCode: + """A block code indicates a data block within a service. A block code + is a 16-bit or 24-bit structure composed of a length bit (1b if + the block number is less than 256), a 3-bit access mode, a 4-bit + service list index and an 8-bit or 16-bit block number. + + """ + def __init__(self, number, access=0, service=0): + self.number = number + self.access = access + self.service = service + + def __repr__(self): + return "BlockCode({0}, {1}, {2})".format( + self.number, self.access, self.service) + + def __str__(self): + s = "BlockCode(number={0}, access={1:03b}, service={2})" + return s.format(self.number, self.access, self.service) + + def __bytes__(self): + return str(self).encode() + + def pack(self): + """Pack the block code for transmission. Returns a 2-3 byte string.""" + bn, am, sx = self.number, self.access, self.service + return bytes( + bytearray([bool(bn < 256) << 7 | (am & 0x7) << 4 | (sx & 0xf)]) + + (bytearray([bn]) if bn < 256 else pack("H", data[14:16])[0]: + log.debug("ndef attribute data checksum error") + return None + + ver, nbr, nbw, nmaxb = unpack(">BBBH", data[0:5]) + writef, rwflag = unpack(">BB", data[9:11]) + length = unpack(">I", b"\x00" + data[11:14])[0] + self._capacity = nmaxb * 16 + self._writeable = rwflag != 0 and nbw > 0 + self._readable = writef == 0 and nbr > 0 + attributes = { + 'ver': ver, 'nbr': nbr, 'nbw': nbw, 'nmaxb': nmaxb, + 'writef': writef, 'rwflag': rwflag, 'ln': length} + log.debug("got ndef attributes {0}".format(attributes)) + return attributes + + def _write_attribute_data(self, attributes): + log.debug("set ndef attributes {0}".format(attributes)) + attribute_data = bytearray(16) + attribute_data[0] = attributes['ver'] + attribute_data[1] = attributes['nbr'] + attribute_data[2] = attributes['nbw'] + attribute_data[3:5] = pack('>H', attributes['nmaxb']) + attribute_data[9] = attributes['writef'] + attribute_data[10] = attributes['rwflag'] + attribute_data[11:14] = pack('>I', attributes['ln'])[1:4] + attribute_data[14:16] = pack('>H', sum(attribute_data[0:14])) + self._tag.write_to_ndef_service(attribute_data, 0) + + def _read_ndef_data(self): + if self.tag.sys != 0x12FC: + try: + self.tag.idm, self.tag.pmm = self._tag.polling(0x12FC) + self.tag.sys = 0x12FC + except Type3TagCommandError: + return None + + attributes = self._read_attribute_data() + if attributes is None: + log.debug("found no attribute data (maybe checksum error)") + return None + if attributes['ver'] >> 4 != 1: + log.debug("unsupported ndef mapping major version") + return None + + last_block_number = 1 + (attributes['ln'] + 15) // 16 + data = bytearray() + + for i in range(1, last_block_number, attributes['nbr']): + last_block = min(i + attributes['nbr'], last_block_number) + block_list = range(i, last_block) + try: + data += self.tag.read_from_ndef_service(*block_list) + except Type3TagCommandError: + return None + + data = data[0:attributes['ln']] + log.debug("got {0} byte ndef data {1}{2}".format( + len(data), + hexlify(data[0:32]).decode(), + ('', '...')[len(data) > 32])) + + return data + + def _write_ndef_data(self, data): + attributes = self._read_attribute_data() + attributes['writef'] = 0x0F + self._write_attribute_data(attributes) + + log.debug("set {0} byte ndef data {1}{2}".format( + len(data), + hexlify(data[0:32]).decode(), + ('', '...')[len(data) > 32])) + + last_block_number = 1 + (len(data) + 15) // 16 + attributes['ln'] = len(data) # because we may need to pad zeros + data = data + bytearray(-len(data) % 16) # adjust to block size + + for i in range(1, last_block_number, attributes['nbw']): + last_block = min(i + attributes['nbw'], last_block_number) + block_data = data[(i-1)*16:(last_block-1)*16] + self._tag.write_to_ndef_service( + block_data, *range(i, last_block)) + + attributes['writef'] = 0x00 + self._write_attribute_data(attributes) + return True + + def __init__(self, clf, target): + super(Type3Tag, self).__init__(clf, target) + self.idm = target.sensf_res[1:9] + self.pmm = target.sensf_res[9:17] + self.sys = 0xFFFF + if len(target.sensf_res) > 17: + self.sys = unpack(">H", target.sensf_res[17:19])[0] + self._nfcid = bytearray(self.idm) + + def __str__(self): + s = " PMM={pmm} SYS={sys:04X}" + return nfc.tag.Tag.__str__(self) + s.format( + pmm=hexlify(self.pmm).decode().upper(), sys=self.sys) + + def _is_present(self): + # Check if the card still responds to the acquired system code + # and the returned identifier (IDm) matches. This is called + # from nfc.tag.Tag for the 'is_present' attribute. + try: + idm, pmm = self.polling(self.sys) + return idm == self.identifier + except Type3TagCommandError: + return False + + def dump(self): + """Read all data blocks of an NFC Forum Tag. + + For an NFC Forum Tag (system code 0x12FC) :meth:`dump` reads + all data blocks from service 0x000B (NDEF read service) and + returns a list of strings suitable for printing. The number of + strings returned does not necessarily reflect the number of + data blocks because a range of data blocks with equal content + is reduced to fewer lines of output. + + """ + if self.sys == 0x12FC: + ndef_read_service = ServiceCode(0, 0b01011) + return self.dump_service(ndef_read_service) + else: + return ["This is not an NFC Forum Tag."] + + def dump_service(self, sc): + """Read all data blocks of a given service. + + :meth:`dump_service` reads all data blocks from the service + with service code *sc* and returns a list of strings suitable + for printing. The number of strings returned does not + necessarily reflect the number of data blocks because a range + of data blocks with equal content is reduced to fewer lines of + output. + + """ + def lprint(fmt, data, index): + ispchr = lambda x: x >= 32 and x <= 126 # noqa: E731 + + def print_bytes(octets): + return ' '.join(['%02x' % x for x in octets]) + + def print_chars(octets): + return ''.join([chr(x) if ispchr(x) else '.' for x in octets]) + + return fmt.format(index, print_bytes(data), print_chars(data)) + + data_line_fmt = "{0:04X}: {1} |{2}|" + same_line_fmt = "{0:<4s} {1} |{2}|" + + lines = list() + last_data = None + same_data = 0 + + for i in itertools.count(): # pragma: no branch + assert i < 0x10000 + try: + this_data = self.read_without_encryption([sc], [BlockCode(i)]) + except Type3TagCommandError: + i = i - 1 + break + + if this_data == last_data: + same_data += 1 + else: + if same_data > 1: + lines.append(lprint(same_line_fmt, last_data, "*")) + lines.append(lprint(data_line_fmt, this_data, i)) + last_data = this_data + same_data = 0 + + if same_data > 1: + lines.append(lprint(same_line_fmt, last_data, "*")) + if same_data > 0: + lines.append(lprint(data_line_fmt, this_data, i)) + + return lines + + def format(self, version=None, wipe=None): + """Format and blank an NFC Forum Type 3 Tag. + + A generic NFC Forum Type 3 Tag can be (re)formatted if it is + in either one of blank, initialized or readwrite state. By + formatting, all contents of the attribute information block is + overwritten with values determined. The number of user data + blocks is determined by reading all memory until an error + response. Similarily, the maximum number of data block that + can be read or written with a single command is determined by + sending successively increased read and write commands. The + current data length is set to zero. The NDEF mapping version + is set to the latest known version number (1.0), unless the + *version* argument is provided and it's major version number + corresponds to one of the known major version numbers. + + By default, no data other than the attribute block is + modified. To overwrite user data the *wipe* argument must be + set to an integer value. The lower 8 bits of that value are + written to all data bytes that follow the attribute block. + + """ + return super(Type3Tag, self).format(version, wipe) + + def _format(self, version, wipe): + assert version is None or type(version) is int + assert wipe is None or type(wipe) is int + + if self.sys != 0x12FC: + log.warning("not an ndef tag and can not be made compatible") + return False + if version and version >> 4 != 1: + log.warning("Type 3 Tag NDEF mapping major version must be 1") + return False + + try: + self.read_from_ndef_service(0) + except Type3TagCommandError: + log.warning("this tag does not have any usable data blocks") + return False + + # To determine the total number of data blocks we start with + # the assumption that it must be between 0 and 2**16, then try + # reading in the middle and adjust the range depending on + # whether the read was successful or not. So in each round we + # have the smallest number that worked and the largest number + # that didn't, obviously the end is when that difference is 1. + """ + nmaxb = [0, 0x10000] + while nmaxb[1] - nmaxb[0] > 1: + block = nmaxb[0] + (nmaxb[1] - nmaxb[0]) // 2 - 1 + try: + self.read_from_ndef_service(block) + except Type3TagCommandError: + nmaxb[1] = block + 1 + else: + nmaxb[0] = block + 1 + """ + nmaxb = [0, 0x10000] + while nmaxb[1] - nmaxb[0] > 1: + print(nmaxb) + block = nmaxb[0] + (nmaxb[1] - nmaxb[0]) // 2 + try: + self.read_from_ndef_service(block) + except Type3TagCommandError: + nmaxb[1] = block + else: + nmaxb[0] = block + + nmaxb = nmaxb[0] + + # To get the number of blocks that can be read in one command + # we just try to read with an increasing number of blocks. + for nbr in range(1, 16): + try: + self.read_from_ndef_service(*(nbr*[0])) + except Type3TagCommandError: + nbr -= 1 + break + + # To get the number of blocks that can be written in one + # command we do essentially the same as for nbr, just that to + # preserve existing data we first read and then write it back. + data = self.read_from_ndef_service(0) + for nbw in range(1, 14): + try: + self.write_to_ndef_service(nbw*data, *(nbw*[0])) + except Type3TagCommandError: + nbw -= 1 + break + + # Tags with more than 4K memory require 3-byte block number + # format. This reduces the maximum number of blocks in write. + if nbw == 13 and nmaxb > 255: + nbw = 12 + + # We now have all information needed to create and write the + # new attribute data to block number 0. + attribute_data = bytearray(16) + attribute_data[0:5] = pack(">BBBH", version, nbr, nbw, nmaxb) + attribute_data[10] = 0x01 if nbw > 0 else 0x00 + attribute_data[14:16] = pack(">H", sum(attribute_data[0:14])) + log.debug("set ndef attributes %s", hexlify(attribute_data).decode()) + self.write_to_ndef_service(attribute_data, 0) + + # If required, we will also overwrite the memory with the + # 8-bit integer provided. This could take a while. + if wipe is not None: + data = bytearray([wipe]) * 16 + while nmaxb > 0: + self.write_to_ndef_service(data, nmaxb) + nmaxb = nmaxb - 1 + + return True + + def polling(self, system_code=0xffff, request_code=0, time_slots=0): + """Aquire and identify a card. + + The Polling command is used to detect the Type 3 Tags in the + field. It is also used for initialization and anti-collision. + + The *system_code* identifies the card system to acquire. A + card can have multiple systems. The first system that matches + *system_code* will be activated. A value of 0xff for any of + the two bytes works as a wildcard, thus 0xffff activates the + very first system in the card. The card identification data + returned are the Manufacture ID (IDm) and Manufacture + Parameter (PMm). + + The *request_code* tells the card whether it should return + additional information. The default value 0 requests no + additional information. Request code 1 means that the card + shall also return the system code, so polling for system code + 0xffff with request code 1 can be used to identify the first + system on the card. Request code 2 asks for communication + performance data, more precisely a bitmap of possible + communication speeds. Not all cards provide that information. + + The number of *time_slots* determines whether there's a chance + to receive a response if multiple Type 3 Tags are in the + field. For the reader the number of time slots determines the + amount of time to wait for a response. Any Type 3 Tag in the + field, i.e. powered by the field, will choose a random time + slot to respond. With the default *time_slots* value 0 there + will only be one time slot available for all responses and + multiple responses would produce a collision. More time slots + reduce the chance of collisions (but may result in an + application working with a tag that was just accidentially + close enough). Only specific values should be used for + *time_slots*, those are 0, 1, 3, 7, and 15. Other values may + produce unexpected results depending on the tag product. + + :meth:`polling` returns either the tuple (IDm, PMm) or the + tuple (IDm, PMm, *additional information*) depending on the + response lengt, all as bytearrays. + + Command execution errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + + log.debug("polling for system 0x{0:04x}".format(system_code)) + if time_slots not in (0, 1, 3, 7, 15): + log.debug("invalid number of time slots: {0}".format(time_slots)) + raise ValueError("invalid number of time slots") + if request_code not in (0, 1, 2): + log.debug("invalid request code value: {0}".format(request_code)) + raise ValueError("invalid request code for polling") + + timeout = 0.003625 + time_slots * 0.001208 + data = pack(">HBB", system_code, request_code, time_slots) + data = self.send_cmd_recv_rsp(0x00, data, timeout, send_idm=False) + if len(data) != (16 if request_code == 0 else 18): + log.debug("unexpected polling response length") + raise Type3TagCommandError(DATA_SIZE_ERROR) + + return (data[0:8], data[8:16]) if len(data) == 16 else \ + (data[0:8], data[8:16], data[16:18]) + + def read_without_encryption(self, service_list, block_list): + """Read data blocks from unencrypted services. + + This method sends a Read Without Encryption command to the + tag. The data blocks to read are indicated by a sequence of + :class:`~nfc.tag.tt3.BlockCode` objects in *block_list*. Each + block code must reference a :class:`~nfc.tag.tt3.ServiceCode` + object from the iterable *service_list*. If any of the blocks + and services do not exist, the tag will stop processing at + that point and return a two byte error status. The status + bytes become the :attr:`~nfc.tag.TagCommandError.errno` value + of the :exc:`~nfc.tag.TagCommandError` exception. + + As an example, the following code reads block 5 from service + 16 (service type 'random read-write w/o key') and blocks 0 to + 1 from service 80 (service type 'random read-only w/o key'):: + + sc1 = nfc.tag.tt3.ServiceCode(16, 0x09) + sc2 = nfc.tag.tt3.ServiceCode(80, 0x0B) + bc1 = nfc.tag.tt3.BlockCode(5, service=0) + bc2 = nfc.tag.tt3.BlockCode(0, service=1) + bc3 = nfc.tag.tt3.BlockCode(1, service=1) + try: + data = tag.read_without_encryption([sc1, sc2], [bc1, bc2, bc3]) + except nfc.tag.TagCommandError as e: + if e.errno > 0x00FF: + print("the tag returned an error status") + else: + print("command failed with some other error") + + Command execution errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + a, b, e = self.pmm[5] & 7, self.pmm[5] >> 3 & 7, self.pmm[5] >> 6 + timeout = 302.1E-6 * ((b + 1) * len(block_list) + a + 1) * 4**e + + data = bytearray([ + len(service_list)]) \ + + b''.join([sc.pack() for sc in service_list]) \ + + bytearray([len(block_list)]) \ + + b''.join([bc.pack() for bc in block_list]) + + log.debug("read w/o encryption service/block list: {0} / {1}".format( + ' '.join([hexlify(sc.pack()).decode() for sc in service_list]), + ' '.join([hexlify(bc.pack()).decode() for bc in block_list]))) + + data = self.send_cmd_recv_rsp(0x06, data, timeout) + + if len(data) != 1 + len(block_list) * 16: + log.debug("insufficient data received from tag") + raise Type3TagCommandError(DATA_SIZE_ERROR) + + return data[1:] + + def read_from_ndef_service(self, *blocks): + """Read block data from an NDEF compatible tag. + + This is a convinience method to read block data from a tag + that has system code 0x12FC (NDEF). For other tags this method + simply returns :const:`None`. All arguments are block numbers + to read. To actually pass a list of block numbers requires + unpacking. The following example calls would have the same + effect of reading 32 byte data from from blocks 1 and 8.:: + + data = tag.read_from_ndef_service(1, 8) + data = tag.read_from_ndef_service(*list(1, 8)) + + Command execution errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + if self.sys == 0x12FC: + sc_list = [ServiceCode(0, 0b001011)] + bc_list = [BlockCode(n) for n in blocks] + return self.read_without_encryption(sc_list, bc_list) + + def write_without_encryption(self, service_list, block_list, data): + """Write data blocks to unencrypted services. + + This method sends a Write Without Encryption command to the + tag. The data blocks to overwrite are indicated by a sequence + of :class:`~nfc.tag.tt3.BlockCode` objects in the parameter + *block_list*. Each block code must reference one of the + :class:`~nfc.tag.tt3.ServiceCode` objects in the iterable + *service_list*. If any of the blocks or services do not exist, + the tag will stop processing at that point and return a two + byte error status. The status bytes become the + :attr:`~nfc.tag.TagCommandError.errno` value of the + :exc:`~nfc.tag.TagCommandError` exception. The *data* to write + must be a byte string or array of length ``16 * + len(block_list)``. + + As an example, the following code writes ``16 * "\\xAA"`` to + block 5 of service 16, ``16 * "\\xBB"`` to block 0 of service + 80 and ``16 * "\\xCC"`` to block 1 of service 80 (all services + are writeable without key):: + + sc1 = nfc.tag.tt3.ServiceCode(16, 0x09) + sc2 = nfc.tag.tt3.ServiceCode(80, 0x09) + bc1 = nfc.tag.tt3.BlockCode(5, service=0) + bc2 = nfc.tag.tt3.BlockCode(0, service=1) + bc3 = nfc.tag.tt3.BlockCode(1, service=1) + sc_list = [sc1, sc2] + bc_list = [bc1, bc2, bc3] + data = 16 * "\\xAA" + 16 * "\\xBB" + 16 * "\\xCC" + try: + data = tag.write_without_encryption(sc_list, bc_list, data) + except nfc.tag.TagCommandError as e: + if e.errno > 0x00FF: + print("the tag returned an error status") + else: + print("command failed with some other error") + + Command execution errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + a, b, e = self.pmm[6] & 7, self.pmm[6] >> 3 & 7, self.pmm[6] >> 6 + timeout = 302.1E-6 * ((b + 1) * len(block_list) + a + 1) * 4**e + + data = bytearray([ + len(service_list)]) \ + + b"".join([sc.pack() for sc in service_list]) \ + + bytearray([len(block_list)]) \ + + b"".join([bc.pack() for bc in block_list]) \ + + bytearray(data) + + log.debug("write w/o encryption service/block list: {0} / {1}".format( + ' '.join([hexlify(sc.pack()).decode() for sc in service_list]), + ' '.join([hexlify(bc.pack()).decode() for bc in block_list]))) + + self.send_cmd_recv_rsp(0x08, data, timeout) + + def write_to_ndef_service(self, data, *blocks): + """Write block data to an NDEF compatible tag. + + This is a convinience method to write block data to a tag that + has system code 0x12FC (NDEF). For other tags this method + simply does nothing. The *data* to write must be a string or + bytearray with length equal ``16 * len(blocks)``. All + parameters following *data* are interpreted as block numbers + to write. To actually pass a list of block numbers requires + unpacking. The following example calls would have the same + effect of writing 32 byte zeros into blocks 1 and 8.:: + + tag.write_to_ndef_service(32 * "\\0", 1, 8) + tag.write_to_ndef_service(32 * "\\0", *list(1, 8)) + + Command execution errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + if self.sys == 0x12FC: + sc_list = [ServiceCode(0, 0b001001)] + bc_list = [BlockCode(n) for n in blocks] + self.write_without_encryption(sc_list, bc_list, data) + + def send_cmd_recv_rsp(self, cmd_code, cmd_data, timeout, + send_idm=True, check_status=True): + """Send a command and receive a response. + + This low level method sends an arbitrary command with the + 8-bit integer *cmd_code*, followed by the captured tag + identifier (IDm) if *send_idm* is :const:`True` and the byte + string or bytearray *cmd_data*. It then waits *timeout* + seconds for a response, verifies that the response is + correctly formatted and, if *check_status* is :const:`True`, + that the status flags do not indicate an error. + + All errors raise a :exc:`~nfc.tag.TagCommandError` + exception. Errors from response status flags produce an + :attr:`~nfc.tag.TagCommandError.errno` that is greater than + 255, all other errors are below 256. + + """ + idm = self.idm if send_idm else bytearray() + cmd = bytearray([2+len(idm)+len(cmd_data), cmd_code]) + idm + cmd_data + log.debug(">> {0:02x} {1:02x} {2} {3} ({4}s)".format( + cmd[0], cmd[1], hexlify(cmd[2:10]).decode(), + hexlify(cmd[10:]).decode(), timeout)) + + started = time.time() + error = None + for retry in range(3): + try: + rsp = self.clf.exchange(cmd, timeout) + break + except nfc.clf.CommunicationError as e: + error = e + reason = error.__class__.__name__ + log.debug("%s after %d retries" % (reason, retry)) + else: + if type(error) is nfc.clf.TimeoutError: + raise Type3TagCommandError(nfc.tag.TIMEOUT_ERROR) + if type(error) is nfc.clf.TransmissionError: + raise Type3TagCommandError(nfc.tag.RECEIVE_ERROR) + if type(error) is nfc.clf.ProtocolError: # pragma: no branch + raise Type3TagCommandError(nfc.tag.PROTOCOL_ERROR) + + if rsp[0] != len(rsp): + log.debug("incorrect response length {0:02x}".format(rsp[0])) + raise Type3TagCommandError(RSP_LENGTH_ERROR) + if rsp[1] != cmd_code + 1: + log.debug("incorrect response code {0:02x}".format(rsp[1])) + raise Type3TagCommandError(RSP_CODE_ERROR) + if send_idm and rsp[2:10] != self.idm: + log.debug("wrong tag or transaction id {}".format( + hexlify(rsp[2:10]).decode())) + raise Type3TagCommandError(TAG_IDM_ERROR) + if not send_idm: + log.debug("<< {0:02x} {1:02x} {2}".format( + rsp[0], rsp[1], hexlify(rsp[2:]).decode())) + return rsp[2:] + if check_status and rsp[10] != 0: + log.debug("tag returned error status {}".format( + hexlify(rsp[10:12]).decode())) + raise Type3TagCommandError(unpack(">H", rsp[10:12])[0]) + if not check_status: + log.debug("<< {0:02x} {1:02x} {2} {3}".format( + rsp[0], rsp[1], hexlify(rsp[2:10]).decode(), + hexlify(rsp[10:]).decode())) + return rsp[10:] + log.debug("<< {0:02x} {1:02x} {2} {3} {4} ({elapsed:f}s)".format( + rsp[0], rsp[1], hexlify(rsp[2:10]).decode(), + hexlify(rsp[10:12]).decode(), hexlify(rsp[12:]).decode(), + elapsed=time.time()-started)) + return rsp[12:] + + +class Type3TagEmulation(nfc.tag.TagEmulation): + """Framework for Type 3 Tag emulation. + + """ + def __init__(self, clf, target): + self.services = dict() + self.target = target + self.cmd = bytearray([len(target.tt3_cmd)+1]) + target.tt3_cmd + self.idm = target.sensf_res[1:9] + self.pmm = target.sensf_res[9:17] + self.sys = target.sensf_res[17:19] + self.clf = clf + + def __str__(self): + """x.__str__() <==> str(x)""" + return "Type3TagEmulation IDm={id} PMm={pmm} SYS={sys}".format( + id=hexlify(self.idm).decode(), + pmm=hexlify(self.pmm).decode(), + sys=hexlify(self.sys).decode()) + + def add_service(self, service_code, block_read_func, block_write_func): + def default_block_read(block_number, rb, re): + return None + + def default_block_write(block_number, block_data, wb, we): + return False + + if block_read_func is None: + block_read_func = default_block_read + + if block_write_func is None: + block_write_func = default_block_write + + self.services[service_code] = (block_read_func, block_write_func) + + def process_command(self, cmd): + log.debug("cmd: %s", hexlify(cmd).decode() if cmd else str(cmd)) + if len(cmd) != cmd[0]: + log.error("tt3 command length error") + return None + if tuple(cmd[0:4]) in [(6, 0, 255, 255), (6, 0) + tuple(self.sys)]: + log.debug("process 'polling' command") + rsp = self.polling(cmd[2:]) + return bytearray([2 + len(rsp), 0x01]) + rsp + if cmd[2:10] == self.idm: + if cmd[1] == 0x04: + log.debug("process 'request response' command") + rsp = self.request_response(cmd[10:]) + return bytearray([10 + len(rsp), 0x05]) + self.idm + rsp + if cmd[1] == 0x06: + log.debug("process 'read without encryption' command") + rsp = self.read_without_encryption(cmd[10:]) + return bytearray([10 + len(rsp), 0x07]) + self.idm + rsp + if cmd[1] == 0x08: + log.debug("process 'write without encryption' command") + rsp = self.write_without_encryption(cmd[10:]) + return bytearray([10 + len(rsp), 0x09]) + self.idm + rsp + if cmd[1] == 0x0C: + log.debug("process 'request system code' command") + rsp = self.request_system_code(cmd[10:]) + return bytearray([10 + len(rsp), 0x0D]) + self.idm + rsp + + def send_response(self, rsp, timeout): + log.debug("rsp: {}".format(hexlify(rsp).decode() + if rsp is not None + else 'None')) + return self.clf.exchange(rsp, timeout) + + def polling(self, cmd_data): + if cmd_data[2] == 1: + rsp = self.idm + self.pmm + self.sys + else: + rsp = self.idm + self.pmm + return rsp + + def request_response(self, cmd_data): + return bytearray([0]) + + def read_without_encryption(self, cmd_data): + service_list = cmd_data.pop(0) * [[None, None]] + for i in range(len(service_list)): + service_code = cmd_data[1] << 8 | cmd_data[0] + if service_code not in self.services.keys(): + return bytearray([0xFF, 0xA1]) + service_list[i] = [service_code, 0] + del cmd_data[0:2] + + service_block_list = cmd_data.pop(0) * [None] + if len(service_block_list) > 15: + return bytearray([0xFF, 0xA2]) + for i in range(len(service_block_list)): + try: + service_list_item = service_list[cmd_data[0] & 0x0F] + service_code = service_list_item[0] + service_list_item[1] += 1 + except IndexError: + return bytearray([1 << (i % 8), 0xA3]) + if cmd_data[0] >= 128: + block_number = cmd_data[1] + del cmd_data[0:2] + else: + block_number = cmd_data[2] << 8 | cmd_data[1] + del cmd_data[0:3] + service_block_list[i] = [service_code, block_number, 0] + + service_block_count = dict(service_list) + for service_block_list_item in service_block_list: + service_code = service_block_list_item[0] + service_block_list_item[2] = service_block_count[service_code] + + block_data = bytearray() + for i, service_block_list_item in enumerate(service_block_list): + service_code, block_number, block_count = service_block_list_item + # rb (read begin) and re (read end) mark an atomic read + rb = bool(block_count == service_block_count[service_code]) + service_block_count[service_code] -= 1 + re = bool(service_block_count[service_code] == 0) + read_func, write_func = self.services[service_code] + one_block_data = read_func(block_number, rb, re) + if one_block_data is None: + return bytearray([1 << (i % 8), 0xA2]) + block_data.extend(one_block_data) + + return bytearray([0, 0, int(math.floor(len(block_data)/16))]) \ + + block_data + + def write_without_encryption(self, cmd_data): + service_list = cmd_data.pop(0) * [[None, None]] + for i in range(len(service_list)): + service_code = cmd_data[1] << 8 | cmd_data[0] + if service_code not in self.services.keys(): + return bytearray([255, 0xA1]) + service_list[i] = [service_code, 0] + del cmd_data[0:2] + + service_block_list = cmd_data.pop(0) * [None] + for i in range(len(service_block_list)): + try: + service_list_item = service_list[cmd_data[0] & 0x0F] + service_code = service_list_item[0] + service_list_item[1] += 1 + except IndexError: + return bytearray([1 << (i % 8), 0xA3]) + if cmd_data[0] >= 128: + block_number = cmd_data[1] + del cmd_data[0:2] + else: + block_number = cmd_data[2] << 8 | cmd_data[1] + del cmd_data[0:3] + service_block_list[i] = [service_code, block_number, 0] + + service_block_count = dict(service_list) + for service_block_list_item in service_block_list: + service_code = service_block_list_item[0] + service_block_list_item[2] = service_block_count[service_code] + + block_data = cmd_data[0:] + if len(block_data) % 16 != 0: + return bytearray([255, 0xA2]) + + for i, service_block_list_item in enumerate(service_block_list): + service_code, block_number, block_count = service_block_list_item + # wb (write begin) and we (write end) mark an atomic write + wb = bool(block_count == service_block_count[service_code]) + service_block_count[service_code] -= 1 + we = bool(service_block_count[service_code] == 0) + read_func, write_func = self.services[service_code] + if not write_func(block_number, block_data[i*16:(i+1)*16], wb, we): + return bytearray([1 << (i % 8), 0xA2]) + + return bytearray([0, 0]) + + def request_system_code(self, cmd_data): + return b'\x01' + self.sys + + +def activate(clf, target): + if not target.sensf_res[1:3] == b"\x01\xFE": + import nfc.tag.tt3_sony + tag = nfc.tag.tt3_sony.activate(clf, target) + return tag if tag else Type3Tag(clf, target) diff --git a/src/lib/nfc/tag/tt3_sony.py b/src/lib/nfc/tag/tt3_sony.py new file mode 100644 index 0000000..9bab877 --- /dev/null +++ b/src/lib/nfc/tag/tt3_sony.py @@ -0,0 +1,987 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2014, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import nfc.tag +from . import tt3 + +import os +import struct +from binascii import hexlify +from pyDes import triple_des, CBC +from struct import pack, unpack +import itertools + +import logging +log = logging.getLogger(__name__) + + +def activate(clf, target): + # http://www.sony.net/Products/felica/business/tech-support/list.html + ic_code = target.sensf_res[10] + if ic_code in FelicaLite.IC_CODE_MAP.keys(): + return FelicaLite(clf, target) + if ic_code in FelicaLiteS.IC_CODE_MAP.keys(): + return FelicaLiteS(clf, target) + if ic_code in FelicaStandard.IC_CODE_MAP.keys(): + return FelicaStandard(clf, target) + if ic_code in FelicaMobile.IC_CODE_MAP.keys(): + return FelicaMobile(clf, target) + if ic_code in FelicaPlug.IC_CODE_MAP.keys(): + return FelicaPlug(clf, target) + return None + + +class FelicaStandard(tt3.Type3Tag): + """Standard FeliCa is a range of FeliCa OS based card products with a + flexible file system that supports multiple applications and + services on the same card. Services can individually be protected + with a card key and all communication with protected services is + encrypted. + + """ + IC_CODE_MAP = { + # IC IC-NAME NBR NBW + 0x00: ("RC-S830", 8, 8), # RC-S831/833 + 0x01: ("RC-S915", 12, 8), # RC-S860/862/863/864/891 + 0x02: ("RC-S919", 1, 1), # RC-S890 + 0x08: ("RC-S952", 12, 8), + 0x09: ("RC-S953", 12, 8), + 0x0B: ("RC-S???", 1, 1), # new suica + 0x0C: ("RC-S954", 12, 8), + 0x0D: ("RC-S960", 12, 10), # RC-S880/889 + 0x20: ("RC-S962", 12, 10), # RC-S885/888/892/893 + 0x32: ("RC-SA00/1", 1, 1), # AES chip + 0x35: ("RC-SA00/2", 1, 1), + } + + def __init__(self, clf, target): + super(FelicaStandard, self).__init__(clf, target) + self._product = "FeliCa Standard ({0})".format( + self.IC_CODE_MAP[self.pmm[1]][0]) + + def _is_present(self): + # Perform a presence check. Modern FeliCa cards implement the + # RequestResponse command, so we'll try that first. If it + # fails we resort the generic way that works for all type 3 + # tags (but resets the card operating mode to zero). + try: + return self.request_response() in (0, 1, 2, 3) + except tt3.Type3TagCommandError: + return super(FelicaStandard, self)._is_present() + + def dump(self): + # Dump the content of a FeliCa card as good as possible. This + # is unfortunately rather complex because we want to reflect + # the area structure with indentation and summarize overlapped + # services under a single item. + + def print_system(system_code): + # Print system information + system_code_map = { + 0x0000: "SDK Sample", + 0x0003: "Suica", + 0x12FC: "NDEF", + 0x811D: "Edy", + 0x8620: "Blackboard", + 0xFE00: "Common Area", + } + return ["System {0:04X} ({1})".format( + system_code, system_code_map.get(system_code, 'unknown'))] + + def print_area(area_from, area_last, depth): + # Prints area information with indentation. + return ["{indent}Area {0:04X}--{1:04X}".format( + area_from, area_last, indent=depth*' ')] + + def print_service(services, depth): + # This function processes a list of overlapped services + # and reads all block data if there is one service that + # does not require a key. First we figure out the common + # service type and which access modes are available. + if services[0] >> 2 & 0b1111 == 0b0010: + service_type = "Random" + access_types = " & ".join([( + "write with key", "write w/o key", + "read with key", "read w/o key")[x & 3] for x in services]) + if services[0] >> 2 & 0b1111 == 0b0011: + service_type = "Cyclic" + access_types = " & ".join([( + "write with key", "write w/o key", + "read with key", "read w/o key")[x & 3] for x in services]) + if services[0] >> 2 & 0b1110 == 0b0100: + service_type = "Purse" + access_types = " & ".join([( + "direct with key", "direct w/o key", + "cashback with key", "cashback w/o key", + "decrement with key", "decrement w/o key", + "read with key", "read w/o key")[x & 7] for x in services]) + # Now we print one line to verbosely describe the service + # and list the service codes. + service_codes = " ".join(["0x{0:04X}".format(x) for x in services]) + lines = [ + "{indent}{type} Service {number}: {access} ({0})".format( + service_codes, indent=depth*' ', type=service_type, + number=services[0] >> 6, access=access_types)] + # The final piece is to see if any of the services allows + # us to read block data without a key. Services w/o key + # have the last bit set to 1, so we generate a list of + # only those services and iterate over the slice from the + # last item to end (that's one or zero services). + for service in [sc for sc in services if sc & 1][-1:]: + sc = tt3.ServiceCode(service >> 6, service & 0b111111) + for line in self.dump_service(sc): + lines.append(depth*' ' + ' ' + line) + return lines + + # Unfortunately there are some older cards with reduced + # command support. If request_system_code() is not supported + # we can only see if the current system code is NDEF and try + # to dup that, otherwise it is the end. + try: + card_system_codes = self.request_system_code() + except nfc.tag.TagCommandError: + if self.sys == 0x12FC: + return super(FelicaStandard, self).dump() + else: + return ["unable to create a memory dump"] + + # A FeliCa card has one or more systems, each system has one + # or more areas which may be nested, and an area may have zero + # to many services. The outer loop iterates over all system + # codes that are present on the card. The inner loop iterates + # by index over all area and service definitions. + lines = [] + for system_code in card_system_codes: + + # A system must be activated first, this is what the + # polling() command does. + idm, pmm = self.polling(system_code) + self.idm = idm + self.pmm = pmm + self.sys = system_code + lines.extend(print_system(system_code)) + + area_stack = [] + overlap_services = [] + + # Walk through the list of services by index. The first + # index for which there is no service returns None and + # terminate the loop. + for service_index in itertools.count(): # pragma: no branch + assert service_index < 0x10000 + depth = len(area_stack) + area_or_service = self.search_service_code(service_index) + if area_or_service is None: + # Went beyond the service index. Print overlap + # services if any and exit loop. + if len(overlap_services) > 0: + lines.extend(print_service(overlap_services, depth)) + overlap_services = [] + break + elif len(area_or_service) == 1: + # Found a service definition. Add as overlap + # service if it is either the first or same type + # (Random, Cyclic, Purse) as the previous one. If + # it is different then print the current overlap + # services and remember this for the next round. + service = area_or_service[0] + end_overlap_services = False + if len(overlap_services) == 0: + overlap_services.append(service) + elif service >> 4 == overlap_services[-1] >> 4: + if service >> 4 & 1: # purse + overlap_services.append(service) + elif service >> 2 == overlap_services[-1] >> 2: + overlap_services.append(service) + else: + end_overlap_services = True + else: + end_overlap_services = True + if end_overlap_services: + lines.extend(print_service(overlap_services, depth)) + overlap_services = [service] + elif len(area_or_service) == 2: + # Found an area definition. Print any services + # that we might so far have assembled, then + # process the area information. + if len(overlap_services) > 0: + lines.extend(print_service(overlap_services, depth)) + overlap_services = [] + area_from, area_last = area_or_service + if len(area_stack) > 0 and area_from > area_stack[-1][1]: + area_stack.pop() + lines.extend(print_area(area_from, area_last, depth)) + area_stack.append((area_from, area_last)) + + return lines + + def request_service(self, service_list): + """Verify existence of a service (or area) and get the key version. + + Each service (or area) to verify must be given as a + :class:`~nfc.tag.tt3.ServiceCode` in the iterable + *service_list*. The key versions are returned as a list of + 16-bit integers, in the order requested. If a specified + service (or area) does not exist, the key version will be + 0xFFFF. + + Command execution errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + a, b, e = self.pmm[2] & 7, self.pmm[2] >> 3 & 7, self.pmm[2] >> 6 + timeout = 302E-6 * ((b + 1) * len(service_list) + a + 1) * 4**e + pack = lambda x: x.pack() # noqa: E731 + data = bytearray([len(service_list)]) \ + + b''.join(map(pack, service_list)) + data = self.send_cmd_recv_rsp(0x02, data, timeout, check_status=False) + if len(data) != 1 + len(service_list) * 2: + log.debug("insufficient data received from tag") + raise tt3.Type3TagCommandError(tt3.DATA_SIZE_ERROR) + return [unpack("> 3 & 7, self.pmm[3] >> 6 + timeout = 302E-6 * (b + 1 + a + 1) * 4**e + data = self.send_cmd_recv_rsp(0x04, b'', timeout, check_status=False) + if len(data) != 1: + log.debug("insufficient data received from tag") + raise tt3.Type3TagCommandError(tt3.DATA_SIZE_ERROR) + return data[0] # mode + + def search_service_code(self, service_index): + """Search for a service code that corresponds to an index. + + The Search Service Code command provides access to the + iterable list of services and areas within the activated + system. The *service_index* argument may be any value from 0 + to 0xffff. As long as there is a service or area found for a + given *service_index*, the information returned is a tuple + with either one or two 16-bit integer elements. Two integers + are returned for an area definition, the first is the area + code and the second is the largest possible service index for + the area. One integer, the service code, is returned for a + service definition. The return value is :const:`None` if the + *service_index* was not found. + + For example, to print all services and areas of the active + system: :: + + for i in xrange(0x10000): + area_or_service = tag.search_service_code(i) + if area_or_service is None: + break + elif len(area_or_service) == 1: + sc = area_or_service[0] + print(nfc.tag.tt3.ServiceCode(sc >> 6, sc & 0x3f)) + elif len(area_or_service) == 2: + area_code, area_last = area_or_service + print("Area {0:04x}--{0:04x}".format(area_code, area_last)) + + Command execution errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + log.debug("search service code index {0}".format(service_index)) + # The maximum response time is given by the value of PMM[3]. + # Some cards (like RC-S860 with IC RC-S915) encode a value + # that is too short, thus we use at lest 2 ms. + a, e = self.pmm[3] & 7, self.pmm[3] >> 6 + timeout = max(302E-6 * (a + 1) * 4**e, 0.002) + data = pack("> 6 + timeout = max(302E-6 * (a + 1) * 4**e, 0.002) + data = self.send_cmd_recv_rsp(0x0C, b'', timeout, check_status=False) + if len(data) != 1 + data[0] * 2: + log.debug("insufficient data received from tag") + raise tt3.Type3TagCommandError(tt3.DATA_SIZE_ERROR) + return [unpack(">H", data[i:i+2])[0] for i in range(1, len(data), 2)] + + +class FelicaMobile(FelicaStandard): + """Mobile FeliCa is a modification of FeliCa for use in mobile + phones. This class does currently not implement anything specific + beyond recognition of the Mobile FeliCa OS version. + + """ + IC_CODE_MAP = { + # IC IC-NAME NBR NBW + 0x06: ("1.0", 1, 1), + 0x07: ("1.0", 1, 1), + 0x10: ("2.0", 1, 1), + 0x11: ("2.0", 1, 1), + 0x12: ("2.0", 1, 1), + 0x13: ("2.0", 1, 1), + 0x14: ("3.0", 1, 1), + 0x15: ("3.0", 1, 1), + 0x16: ("3.0", 1, 1), + 0x17: ("3.0", 1, 1), + 0x18: ("3.0", 1, 1), + 0x19: ("3.0", 1, 1), + 0x1A: ("3.0", 1, 1), + 0x1B: ("3.0", 1, 1), + 0x1C: ("3.0", 1, 1), + 0x1D: ("3.0", 1, 1), + 0x1E: ("3.0", 1, 1), + 0x1F: ("3.0", 1, 1), + } + + def __init__(self, clf, target): + super(FelicaMobile, self).__init__(clf, target) + self._product = "FeliCa Mobile " + self.IC_CODE_MAP[self.pmm[1]][0] + + +class FelicaLite(tt3.Type3Tag): + """FeliCa Lite is a version of FeliCa with simplified file system and + security functions. The usable memory is 13 blocks (one block has + 16 byte) plus a one block subtraction register. The tag can be + configured with a card key to authenticate the tag and protect + integrity of data reads. + + """ + IC_CODE_MAP = { + 0xF0: "FeliCa Lite (RC-S965)", + } + + class NDEF(tt3.Type3Tag.NDEF): + def _read_attribute_data(self): + log.debug("FelicaLite.read_attribute_data") + attributes = super(FelicaLite.NDEF, self)._read_attribute_data() + if attributes is not None and self._tag.is_authenticated: + # when authenticated we need to make room for the mac + self._original_nbr = attributes['nbr'] + attributes['nbr'] = min(attributes['nbr'], 3) + return attributes + + def _write_attribute_data(self, attributes): + log.debug("FelicaLite.read_attribute_data") + if self._tag.is_authenticated: + attributes = attributes.copy() + attributes['nbr'] = self._original_nbr + super(FelicaLite.NDEF, self)._write_attribute_data(attributes) + + def __init__(self, clf, target): + super(FelicaLite, self).__init__(clf, target) + self._product = self.IC_CODE_MAP[self.pmm[1]] + self._sk = self._iv = None + self.read_from_ndef_service = self.read_without_mac + self.write_to_ndef_service = self.write_without_mac + + def dump(self): + def oprint(octets): + return ' '.join(['%02x' % x for x in octets]) + + def cprint(octets): + return ''.join([chr(x) if 32 <= x <= 126 else '.' for x in octets]) + + userblocks = list() + for i in range(0, 14): + try: + data = self.read_without_mac(i) + except tt3.Type3TagCommandError: + userblocks.append("{0} |{1}|".format( + " ".join(16 * ["??"]), 16*".")) + else: + userblocks.append("{0} |{1}|".format( + oprint(data), cprint(data))) + + lines = list() + last_block = None + same_blocks = 0 + + for i, block in enumerate(userblocks): + if block == last_block: + same_blocks += 1 + continue + if same_blocks: + if same_blocks > 1: + lines.append(" * " + last_block) + same_blocks = 0 + lines.append("{0:3}: ".format(i) + block) + last_block = block + + if same_blocks: + if same_blocks > 1: + lines.append(" * " + last_block) + lines.append("{0:3}: ".format(i) + block) + + data = self.read_without_mac(14) + lines.append(" 14: {0} ({1})".format(oprint(data), "REGA[4]B[4]C[8]")) + + text = ("RC1[8], RC2[8]", "MAC[8]", "IDD[8], DFC[2]", + "IDM[8], PMM[8]", "SERVICE_CODE[2]", + "SYSTEM_CODE[2]", "CKV[2]", "CK1[8], CK2[8]", + "MEMORY_CONFIG") + config = dict(zip(range(0x80, 0x80+len(text)), text)) + + for i in sorted(config.keys()): + try: + data = self.read_without_mac(i) + except tt3.Type3TagCommandError: + lines.append("{0:3}: {1}({2})".format( + i, 16 * "?? ", config[i])) + else: + lines.append("{0:3}: {1} ({2})".format( + i, oprint(data), config[i])) + + return lines + + @staticmethod + def generate_mac(data, key, iv, flip_key=False): + # Data is first split into tuples of 8 character bytes, each + # tuple then reversed and joined, finally all joined back to + # one string that is then triple des encrypted with key and + # initialization vector iv. If flip_key is True then the key + # halfs will be exchanged (this is used to generate a mac for + # write). The resulting mac is the last 8 bytes returned in + # reversed order. + assert len(data) % 8 == 0 and len(key) == 16 and len(iv) == 8 + key = bytes(key[8:] + key[:8]) if flip_key else bytes(key) + txt = b''.join([ + struct.pack("{}B".format(len(x)), *reversed(x)) + if isinstance(x[0], int) + else b''.join(reversed(x)) + for x in zip(*[iter(bytes(data))]*8)]) + return bytearray(triple_des(key, CBC, bytes(iv)).encrypt(txt)[:-9:-1]) + + def protect(self, password=None, read_protect=False, protect_from=0): + """Protect a FeliCa Lite Tag. + + A FeliCa Lite Tag can be provisioned with a custom password + (or the default manufacturer key if the password is an empty + string or bytearray) to ensure that data retrieved by future + read operations, after authentication, is genuine. Read + protection is not supported. + + A non-empty *password* must provide at least 128 bit key + material, in other words it must be a string or bytearray of + length 16 or more. + + The memory unit for the value of *protect_from* is 16 byte, + thus with ``protect_from=2`` bytes 0 to 31 are not protected. + If *protect_from* is zero (the default value) and the Tag has + valid NDEF management data, the NDEF RW Flag is set to read + only. + + """ + return super(FelicaLite, self).protect( + password, read_protect, protect_from) + + def _protect(self, password, read_protect, protect_from): + if password and len(password) < 16: + raise ValueError("password must be at least 16 byte") + + if protect_from < 0: + raise ValueError("protect_from can not be negative") + + if read_protect: + log.info("this tag can not be made read protected") + return False + + # The memory configuration block contains access permissions + # and ndef compatibility information. + mc = self.read_without_mac(0x88) + + if password is not None: + if mc[2] != 0xFF: + log.info("system block protected, can't write key") + return False + + # if password is empty use factory key of 16 zero bytes + key = password[0:16] if password else b"\0"*16 + + log.debug("protect with key %s", hexlify(key).decode()) + self.write_without_mac(key[7::-1] + key[15:7:-1], 0x87) + + if protect_from < 14: + log.debug("write protect blocks {0}--13".format(protect_from)) + mc[0:2] = pack("H', sum(attribute_data[0:14])) + self.write_without_mac(attribute_data, 0) + + log.debug("write protect system blocks 82,83,84,86,87") + mc[2] = 0x00 # set system blocks 82,83,84,86,87 to read only + + log.debug("write memory configuration %s", hexlify(mc).decode()) + self.write_without_mac(mc, 0x88) + return True + + def authenticate(self, password): + """Authenticate a FeliCa Lite Tag. + + A FeliCa Lite Tag is authenticated by a procedure that allows + both the reader and the tag to calculate a session key from a + random challenge send by the reader and a key that is securely + stored on the tag and provided to :meth:`authenticate` as the + *password* argument. If the tag was protected with an earlier + call to :meth:`protect` then the same password should + successfully authenticate. + + After authentication the :meth:`read_with_mac` method can be + used to read data such that it can not be falsified on + transmission. + + """ + return super(FelicaLite, self).authenticate(password) + + def _authenticate(self, password): + if password and len(password) < 16: + raise ValueError("password must be at least 16 byte") + + # Perform internal authentication, i.e. ensure that the tag + # has the same card key as in password. If the password is + # empty, we'll try with the factory key. + key = b"\0" * 16 if not password else password[0:16] + + log.debug("authenticate with key {}".format(hexlify(key).decode())) + self._authenticated = False + self.read_from_ndef_service = self.read_without_mac + self.write_to_ndef_service = self.write_without_mac + + # Internal authentication starts with a random challenge (rc1 || rc2) + # that we write to the rc block. Because the tag works little endian, + # we reverse the order of rc1 and rc2 bytes when writing. + rc = os.urandom(16) + log.debug("rc1 = {}".format(hexlify(rc[:8]).decode())) + log.debug("rc2 = {}".format(hexlify(rc[8:]).decode())) + self.write_without_mac(rc[7::-1] + rc[15:7:-1], 0x80) + + # The session key becomes the triple_des encryption of the random + # challenge under the card key and with an initialization vector of + # all zero. + sk = triple_des(key, CBC, b'\00' * 8).encrypt(rc) + log.debug("sk1 = {}".format(hexlify(sk[:8]).decode())) + log.debug("sk2 = {}".format(hexlify(sk[8:]).decode())) + + # By reading the id and mac block together we get the mac that the + # tag has generated over the id block data under it's session key + # generated the same way as we did) and with rc1 as the + # initialization vector. + data = self.read_without_mac(0x82, 0x81) + + # Now we check if we calculate the same mac with our session key. + # Note that, because of endianess, data must be reversed in chunks + # of 8 bytes as does the 8 byte mac - this is all done within the + # generate_mac() function. + if data[-16:-8] == self.generate_mac(data[0:-16], sk, iv=rc[0:8]): + log.debug("tag authentication completed") + self._sk = sk + self._iv = rc[0:8] + self._authenticated = True + self.read_from_ndef_service = self.read_with_mac + else: + log.debug("tag authentication failed") + + return self._authenticated + + def format(self, version=0x10, wipe=None): + """Format a FeliCa Lite Tag for NDEF. + + """ + return super(FelicaLite, self).format(version, wipe) + + def _format(self, version, wipe): + assert type(version) is int + assert wipe is None or type(wipe) is int + + if version and version >> 4 != 1: + log.error("type 3 tag ndef mapping major version must be 1") + return False + + # The memory configuration block contains access permissions + # and ndef compatibility information. + mc = self.read_without_mac(0x88) + + if mc[0] & 0x01 != 0x01: + log.info("the first user data block is not writeable") + return False + + if not mc[3] & 0x01: # ndef compatibility flag + if mc[2] == 0xFF: # mc block is writeable + mc[3] = mc[3] | 0x01 + self.write_without_mac(mc, 0x88) + else: + log.info("this tag can no longer be changed to ndef") + return False + + # Count the number of writeable data blocks (that is excluding + # the attribute block) from the least significant read/write + # permission bits that are consecutively set to 1. + rw_bits = unpack("> (nmaxb + 1) & 1 == 0: + break + + # Create and write the attribute data. Version number, Nbr and + # Nbw are fix and we have just determined Nmaxb. + attribute_data = bytearray(16) + attribute_data[:14] = pack(">BBBHxxxxxBxxx", version, 4, 1, nmaxb, 1) + attribute_data[14:] = pack(">H", sum(attribute_data[:14])) + log.debug("set ndef attributes %s", hexlify(attribute_data).decode()) + self.write_without_mac(attribute_data, 0) + + # Overwrite the ndef message area if a wipe is requested. + if wipe is not None: + data = bytearray(16 * [wipe]) + for block in range(1, nmaxb+1): + self.write_without_mac(data, block) + + return True + + def read_without_mac(self, *blocks): + """Read a number of data blocks without integrity check. + + This method accepts a variable number of integer arguments as + the block numbers to read. The blocks are read with service + code 0x000B (NDEF). + + Tag command errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + log.debug("read {0} block(s) without mac".format(len(blocks))) + service_list = [tt3.ServiceCode(0, 0b001011)] + block_list = [tt3.BlockCode(n) for n in blocks] + return self.read_without_encryption(service_list, block_list) + + def read_with_mac(self, *blocks): + """Read a number of data blocks with integrity check. + + This method accepts a variable number of integer arguments as + the block numbers to read. The blocks are read with service + code 0x000B (NDEF). Along with the requested block data the + tag returns a message authentication code that is verified + before data is returned. If verification fails the return + value of :meth:`read_with_mac` is None. + + A :exc:`RuntimeError` exception is raised if the tag was not + authenticated before calling this method. + + Tag command errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + log.debug("read {0} block(s) with mac".format(len(blocks))) + + if self._sk is None or self._iv is None: + raise RuntimeError("authentication required") + + service_list = [tt3.ServiceCode(0, 0b001011)] + block_list = [tt3.BlockCode(n) for n in blocks] + block_list.append(tt3.BlockCode(0x81)) + + data = self.read_without_encryption(service_list, block_list) + data, mac = data[0:-16], data[-16:-8] + if mac != self.generate_mac(data, self._sk, self._iv): + log.warning("mac verification failed") + else: + return data + + def write_without_mac(self, data, block): + """Write a data block without integrity check. + + This is the standard write method for a FeliCa Lite. The + 16-byte string or bytearray *data* is written to the numbered + *block* in service 0x0009 (NDEF write service). :: + + data = bytearray(range(16)) # 0x00, 0x01, ... 0x0F + try: tag.write_without_mac(data, 5) # write block 5 + except nfc.tag.TagCommandError: + print("something went wrong") + + Tag command errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + # Write a single data block without a mac. Write with mac is + # only supported by FeliCa Lite-S. + assert len(data) == 16 and type(block) is int + log.debug("write 1 block without mac".format()) + sc_list = [tt3.ServiceCode(0, 0b001001)] + bc_list = [tt3.BlockCode(block)] + self.write_without_encryption(sc_list, bc_list, data) + + +class FelicaLiteS(FelicaLite): + """FeliCa Lite-S is a version of FeliCa Lite with enhanced security + functions. It provides mutual authentication were both the tag and + the reader must demonstrate posession of the card key before data + writes can be made. It is also possible to require mutual + authentication for data reads. + + """ + IC_CODE_MAP = { + 0xF1: "FeliCa Lite-S (RC-S966)", + 0xF2: "FeliCa Link (RC-S730) Lite-S Mode", + } + + class NDEF(FelicaLite.NDEF): + def _read_attribute_data(self): + log.debug("FelicaLiteS.read_attribute_data") + attributes = super(FelicaLiteS.NDEF, self)._read_attribute_data() + if attributes is not None and self._tag._authenticated: + # when authenticated and user data is writeable + mc = self._tag.read_without_mac(0x88) + rw_bits = unpack("H', sum(attribute_data[0:14])) + self.write_without_mac(attribute_data, 0) + + log.debug("write protect system blocks 82,83,84,86,87") + mc[2] = 0x00 # set system blocks 82,83,84,86,87 to read only + mc[5] = 0x01 # but allow write with mac to ck and ckv block + + # Write the new memory control block. + log.debug("write memory configuration %s", hexlify(mc).decode()) + self.write_without_mac(mc, 0x88) + return True + + def authenticate(self, password): + """Mutually authenticate with a FeliCa Lite-S Tag. + + FeliCa Lite-S supports enhanced security functions, one of + them is the mutual authentication performed by this + method. The first part of mutual authentication is to + authenticate the tag with :meth:`FelicaLite.authenticate`. If + successful, the shared session key is used to generate the + integrity check value for write operation to update a specific + memory block. If that was successful then the tag is ensured + that the reader has the correct card key. + + After successful authentication the + :meth:`~FelicaLite.read_with_mac` and :meth:`write_with_mac` + methods can be used to read and write data such that it can + not be falsified on transmission. + + """ + if super(FelicaLiteS, self).authenticate(password): + # At this point we have achieved internal authentication, + # i.e we know that the tag has the same card key as in + # password. We now reset the authentication status and do + # external authentication to assure the tag that we have + # the right card key. + self._authenticated = False + self.read_from_ndef_service = self.read_without_mac + self.write_to_ndef_service = self.write_without_mac + + # To authenticate to the tag we write a 01h into the + # ext_auth byte of the state block (block 0x92). The other + # bytes of the state block can be all set to zero. + self.write_with_mac(b"\x01" + 15*b"\0", 0x92) + + # Now read the state block and check the value of the + # ext_auth to see if we are authenticated. If it's 01h + # then we are, otherwise not. + if self.read_with_mac(0x92)[0] == 0x01: + log.debug("mutual authentication completed") + self._authenticated = True + self.read_from_ndef_service = self.read_with_mac + self.write_to_ndef_service = self.write_with_mac + else: + log.debug("mutual authentication failed") + + return self._authenticated + + def write_with_mac(self, data, block): + """Write one data block with additional integrity check. + + If prior to calling this method the tag was not authenticated, + a :exc:`RuntimeError` exception is raised. + + Command execution errors raise :exc:`~nfc.tag.TagCommandError`. + + """ + # Write a single data block protected with a mac. The card + # will only accept the write if it computed the same mac. + log.debug("write 1 block with mac") + if len(data) != 16: + raise ValueError("data must be 16 octets") + if type(block) is not int: + raise ValueError("block number must be int") + if self._sk is None or self._iv is None: + raise RuntimeError("tag must be authenticated first") + + # The write count is the first three byte of the wcnt block. + wcnt = self.read_without_mac(0x90)[0:3] + log.debug("write count is %s", hexlify(wcnt[::-1]).decode()) + + # We must generate the mac_a block to write the data. The data + # to encrypt to the mac is composed of write count and block + # numbers (8 byte) and the data we want to write. The mac for + # write must be generated with the key flipped (sk2 || sk1). + def flip(sk): + return sk[8:16] + sk[0:8] + + data = wcnt + b"\x00" + bytearray([block]) + b"\x00\x91\x00" + data + maca = self.generate_mac(data, flip(self._sk), self._iv) + wcnt+5*b"\0" + + # Now we can write the data block with our computed mac to the + # desired block and the maca block. Write without encryption + # means that the data is not encrypted with a service key. + sc_list = [tt3.ServiceCode(0, 0b001001)] + bc_list = [tt3.BlockCode(block), tt3.BlockCode(0x91)] + self.write_without_encryption(sc_list, bc_list, data[8:24] + maca) + + +class FelicaPlug(tt3.Type3Tag): + """FeliCa Plug is a contactless communication interface module for + microcontrollers. + + """ + IC_CODE_MAP = { + 0xE0: "FeliCa Plug (RC-S926)", + 0xE1: "FeliCa Link (RC-S730) Plug Mode", + } + + def __init__(self, clf, target): + super(FelicaPlug, self).__init__(clf, target) + self._product = self.IC_CODE_MAP[self.pmm[1]] diff --git a/src/lib/nfc/tag/tt4.py b/src/lib/nfc/tag/tt4.py new file mode 100644 index 0000000..08267eb --- /dev/null +++ b/src/lib/nfc/tag/tt4.py @@ -0,0 +1,579 @@ +# -*- coding: latin-1 -*- +# ----------------------------------------------------------------------------- +# Copyright 2012, 2017 Stephen Tiedemann +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# ----------------------------------------------------------------------------- +import itertools +from binascii import hexlify +from struct import pack, unpack + +import nfc.tag +import nfc.clf + +import logging +log = logging.getLogger(__name__) + + +ndef_aid_v1 = bytearray.fromhex("D2760000850100") +ndef_aid_v2 = bytearray.fromhex("D2760000850101") + + +class Type4TagCommandError(nfc.tag.TagCommandError): + """Type 4 Tag exception class. Beyond the generic error values from + :attr:`~nfc.tag.TagCommandError` this class covers ISO 7816-4 + response APDU error codes. + + """ + errno_str = { + # ISO/IEC 7816-4 (2005) APDU errors (SW1/SW2) + 0x6700: "wrong lenght (general error)", + 0x6900: "command not allowed (general error)", + 0x6981: "command incompatible with file structure", + 0x6982: "security status not satisfied", + 0x6A00: "wrong parameters p1/p2 (general error)", + 0x6A80: "incorrect parameters in command data field", + 0x6A81: "function not supported", + 0x6A82: "file or application not found", + 0x6A83: "record not found", + 0x6A84: "not enough memory space in the file", + 0x6A85: "command length inconsistent with TLV structure", + 0x6A86: "incorrect parameters p1/p2", + 0x6A87: "command length inconsistent with p1/p2", + 0x6A88: "referenced data or reference data not found", + 0x6A89: "file already exists", + 0x6A8A: "file name already exists", + } + + @staticmethod + def from_status(status): + return Type4TagCommandError(unpack(">H", status)[0]) + + +class IsoDepInitiator(object): + def __init__(self, clf, fsc, fwt): + self.clf = clf + self.pni = 0 + self.miu = fsc - 3 # account for 1 byte PCB and 2 byte EDC + self.fwt = fwt + self.delta_fwt = 49152 / 13.56E6 + self.n_retry_ack = min(int(1/self.fwt), 5) + self.n_retry_nak = self.n_retry_ack + + def exchange(self, command, timeout=None): + if timeout is None: + timeout = self.fwt + self.delta_fwt + + if command is None: + # presence check with R(NAK) + data = bytearray([0xB2 | self.pni]) + self.clf.exchange(data, timeout) + return + + for offset in range(0, len(command), self.miu): + more = len(command) - offset > self.miu + pfb = pack('B', (0x02, 0x12)[more] | self.pni) + data = pfb + command[offset:offset+self.miu] + + for i in itertools.count(start=1): # pragma: no branch + try: + data = self.clf.exchange(data, timeout) + if len(data) == 0: + raise nfc.clf.TransmissionError + if data[0] == 0xA2 | (~self.pni & 1): + log.debug("ISO-DEP retransmit after ack") + data = pfb + command[offset:offset+self.miu] + continue + break + except nfc.clf.TransmissionError: + if i <= self.n_retry_nak: + log.warning("ISO-DEP transmission error (#%d)" % i) + data = bytearray([0xB2 | self.pni]) + else: + log.error("ISO-DEP unrecoverable transmission error") + raise Type4TagCommandError(nfc.tag.RECEIVE_ERROR) + except nfc.clf.TimeoutError: + if i <= self.n_retry_nak: + log.warning("ISO-DEP timeout error (#%d)" % i) + data = bytearray([0xB2 | self.pni]) + else: + log.error("ISO-DEP unrecoverable timeout error") + raise Type4TagCommandError(nfc.tag.TIMEOUT_ERROR) + except nfc.clf.ProtocolError: + log.error("ISO-DEP unrecoverable protocol error") + raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR) + + while data[0] & 0b11111110 == 0b11110010: # WTX + log.debug("ISO-DEP waiting time extension") + data = self.clf.exchange(data, (data[1] & 0x3F) * self.fwt) + + if data[0] & 0x01 != self.pni: + log.warning("ISO-DEP protocol error: block number") + raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR) + + if more: + if data[0] & 0b11111110 == 0b10100010: # ACK + self.pni = (self.pni + 1) % 2 + else: + log.error("ISO-DEP protocol error: expected ack") + raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR) + else: + if data[0] & 0b11101110 == 0x02: # INF + self.pni = (self.pni + 1) % 2 + response = data[1:] + else: + log.error("ISO-DEP protocol error: expected inf") + raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR) + + while bool(data[0] & 0b00010000): + data = pack('B', 0xA2 | self.pni) # ACK + + for i in itertools.count(start=1): # pragma: no branch + try: + data = self.clf.exchange(data, timeout) + if len(data) == 0: + raise nfc.clf.TransmissionError + break + except nfc.clf.TransmissionError: + if i <= self.n_retry_ack: + log.warning("ISO-DEP transmission error (#%d)" % i) + data = bytearray([0xA2 | self.pni]) + else: + log.error("ISO-DEP unrecoverable transmission error") + raise Type4TagCommandError(nfc.tag.RECEIVE_ERROR) + except nfc.clf.TimeoutError: + if i <= self.n_retry_ack: + log.warning("ISO-DEP timeout error (#%d)" % i) + data = bytearray([0xA2 | self.pni]) + else: + log.error("ISO-DEP unrecoverable timeout error") + raise Type4TagCommandError(nfc.tag.TIMEOUT_ERROR) + except nfc.clf.ProtocolError: + log.error("ISO-DEP unrecoverable protocol error") + raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR) + + if data[0] & 0x01 != self.pni: + log.error("ISO-DEP protocol error: block number") + raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR) + + response = response + data[1:] + self.pni = (self.pni + 1) % 2 + + return response + + +class Type4Tag(nfc.tag.Tag): + """Implementation of the NFC Forum Type 4 Tag operation specification. + + The NFC Forum Type 4 Tag is based on ISO/IEC 14443 DEP protocol + for Type A and B modulation and uses ISO/IEC 7816-4 command and + response APDUs. + + """ + TYPE = "Type4Tag" + + class NDEF(nfc.tag.Tag.NDEF): + # Type 4 Tag specific implementation of the NDEF access type + # class that is returned by the Tag.ndef attribute. + + def _select_ndef_application(self): + for self._aid, mrl in ((ndef_aid_v2, 256), (ndef_aid_v1, 0)): + try: + self.tag.send_apdu(0, 0xA4, 0x04, 0x00, self._aid, mrl) + log.debug("selected %s", hexlify(self._aid).decode()) + return True + except Type4TagCommandError as error: + if error.errno <= 0: + break + + def _select_fid(self, fid): + p2 = 0x00 if self._aid == ndef_aid_v1 else 0x0C + try: + self.tag.send_apdu(0, 0xA4, 0x00, p2, fid) + log.debug("selected %s", hexlify(fid).decode()) + return True + except Type4TagCommandError: + log.debug("failed to select %s", hexlify(fid).decode()) + + def _read_binary(self, offset, size): + (p1, p2) = pack(">H", offset) + max_data = min(self._max_le, size) + log.debug("read_binary from %d to %d", offset, offset + max_data) + return self.tag.send_apdu(0, 0xB0, p1, p2, mrl=max_data) + + def _update_binary(self, offset, data): + (p1, p2) = pack(">H", offset) + max_data = min(self._max_lc, len(data)) + log.debug("update_binary from %d to %d", offset, offset + max_data) + self.tag.send_apdu(0, 0xD6, p1, p2, data[:max_data]) + return max_data + + def _discover_ndef(self): + self._max_lc = 1 + self._max_le = 15 + + log.debug("select ndef application") + if not self._select_ndef_application(): + log.debug("no ndef application file") + return False + + log.debug("select ndef capability file") + if not self._select_fid(b"\xE1\x03"): + log.warning("no ndef capability file") + return False + + log.debug("read ndef capability file") + cclen = self._read_binary(0, 2) + if not (cclen and len(cclen) == 2): + log.debug("error reading capability length") + return False + + cclen = unpack(">H", cclen)[0] + capabilities = self._read_binary(2, min(cclen-2, 15)) + + if capabilities is None or len(capabilities) < 13: + log.warning("insufficient capability data") + return False + + capabilities += (15-len(capabilities)) * b"\0" # for unpack + ver, mle, mlc, tag, val = unpack(">BHHB9p", capabilities) + log.debug("ndef mapping version %d.%d", ver >> 4, ver & 15) + log.debug("max apdu response length %d", mle) + log.debug("max apdu command length %d", mlc) + log.debug("ndef file control tlv tag %d", tag) + + if ver >> 4 not in (1, 2, 3): + log.debug("unsupported major ndef version") + return False + + if not (tag, len(val)) in ((4, 6), (6, 8)): + log.error("invalid ndef control tlv") + return False + + ndef_control_tlv_format = ">2sHBB" if tag == 4 else ">2sIBB" + ndef_file, mfs, rf, wf = unpack(ndef_control_tlv_format, val) + log.debug("ndef file identifier %s", hexlify(ndef_file).decode()) + log.debug("ndef file size limit %d", mfs) + log.debug("ndef file read flag is %d", rf) + log.debug("ndef file write flag is %d", wf) + + self._max_le = mle + self._max_lc = mlc + self._capacity = mfs - tag + 2 + self._readable = bool(rf == 0) + self._writeable = bool(wf == 0) + self._nlen_size = tag - 2 + self._ndef_file = ndef_file + + return True + + def _read_ndef_data(self): + log.debug("read ndef data") + + try: + if not (hasattr(self, "_ndef_file") or self._discover_ndef()): + log.debug("no ndef application") + return None + + log.debug("select ndef data file") + if not self._select_fid(self._ndef_file): + log.warning("ndef file select error") + return None + + log.debug("read ndef data file") + lfmt = ">I" if self._nlen_size == 4 else ">H" + nlen = self._read_binary(0, self._nlen_size) + if len(nlen) != self._nlen_size: + return None + + nlen = unpack(lfmt, nlen)[0] + log.debug("ndef data length is {0}".format(nlen)) + + data = bytearray() + while len(data) < nlen: + offset = self._nlen_size + len(data) + data += self._read_binary(offset, nlen - len(data)) + + except Type4TagCommandError: + return None + else: + return data + + def _write_ndef_data(self, data): + log.debug("write ndef data") + + lfmt = ">I" if self._nlen_size == 4 else ">H" + nlen = bytearray(pack(lfmt, len(data))) + if len(nlen) + len(data) <= self._max_lc: + data = bytearray(nlen) + data + nlen = None + else: + data = bytearray(len(nlen)) + data + + offset = 0 + while offset < len(data): + offset += self._update_binary(offset, data[offset:]) + + if nlen: + self._update_binary(0, nlen) + + return True + + def _wipe_ndef_data(self, wipe=None): + lfmt = ">I" if self._nlen_size == 4 else ">H" + nlen = bytearray(pack(lfmt, 0)) + self._update_binary(0, nlen) + offset = self._nlen_size + data = bytearray(self._capacity * [wipe % 256]) + while offset < self.capacity: + offset += self._update_binary(offset, data[offset:]) + + def _dump_ndef_data(self): + lines = [] + for offset in itertools.count(0, 16): # pragma: no branch + try: + line = self._read_binary(offset, 16) + if len(line) > 0: + lines.append(line) + if len(line) < 16: + break + except Type4TagCommandError: + break + + return lines + + def _is_present(self): + try: + self._dep.exchange(None) + return True + except nfc.clf.CommunicationError: + return False + + def dump(self): + """Returns tag data as a list of formatted strings. + + The :meth:`dump` method provides useful output only for NDEF + formatted Type 4 Tags. Each line that is returned contains a + hexdump of 16 octets from the NDEF data file. + + """ + return self._dump() + + def _dump(self): + def oprint(octets): + return ' '.join(['%02x' % x for x in octets]) + + def cprint(octets): + return ''.join([chr(x) if 32 <= x <= 126 else '.' for x in octets]) + + def lprint(fmt, octets, index): + return fmt.format(index, oprint(octets), cprint(octets)) + + lfmt = "0x{0:04x}: {1} |{2}|" + + if self.ndef and self.ndef.is_readable: + lines = self.ndef._dump_ndef_data() + return [lprint(lfmt, d, i << 4) for i, d in enumerate(lines)] + + return [] + + def format(self, version=None, wipe=None): + """Erase the NDEF message on a Type 4 Tag. + + The :meth:`format` method writes the length of the NDEF + message on a Type 4 Tag to zero, thus the tag will appear to + be empty. If the *wipe* argument is set to some integer then + :meth:`format` will also overwrite all user data with that + integer (mod 256). + + Despite it's name, the :meth:`format` method can not format a + blank tag to make it NDEF compatible; this requires + proprietary information from the manufacturer. + + """ + return super(Type4Tag, self).format(version, wipe) + + def _format(self, version, wipe): + if not self.ndef or not self.ndef.is_writeable: + log.error("format error: no ndef or not writeable") + return False + + if wipe is not None: + try: + self.ndef._wipe_ndef_data(wipe) + except Type4TagCommandError as error: + log.error("format error: %s", str(error)) + return False + + return True + + def transceive(self, data, timeout=None): + """Transmit arbitrary data and receive the response. + + This is a low level method to send arbitrary data to the + tag. While it should almost always be better to use + :meth:`send_apdu` this is the only way to force a specific + timeout value (which is otherwise derived from the Tag's + answer to select). The *timeout* value is expected as a float + specifying the seconds to wait. + + """ + log.debug(">> {0}".format(hexlify(data).decode())) + data = self._dep.exchange(data, timeout) + log.debug("<< {0}".format(hexlify(data).decode() if data else "None")) + return data + + def send_apdu(self, cla, ins, p1, p2, data=None, mrl=0, check_status=True): + """Send an ISO/IEC 7816-4 APDU to the Type 4 Tag. + + The 4 byte APDU header (class, instruction, parameter 1 and 2) + is constructed from the first four parameters (cla, ins, p1, + p2) without interpretation. The byte string *data* argument + represents the APDU command data field. It is encoded as a + short or extended length field followed by the *data* + bytes. The length field is not transmitted if *data* is None + or an empty string. The maximum acceptable number of response + data bytes is given with the max-response-length *mrl* + argument. The value of *mrl* is transmitted as the 7816-4 APDU + Le field after appropriate conversion. + + By default, the response is returned as a byte array not + including the status word, a :exc:`Type4TagCommandError` + exception is raised for any status word other than + 9000h. Response status verification can be disabled with + *check_status* set to False, the byte array will then include + the response status word at the last two positions. + + Transmission errors always raise a :exc:`Type4TagCommandError` + exception. + + """ + apdu = bytearray([cla, ins, p1, p2]) + + if not self._extended_length_support: + if data and len(data) > 255: + raise ValueError("unsupported command data length") + if mrl and mrl > 256: + raise ValueError("unsupported max response length") + if data: + apdu += pack('>B', len(data)) + bytes(data) + if mrl > 0: + apdu += pack('>B', 0 if mrl == 256 else mrl) + else: + if data and len(data) > 65535: + raise ValueError("invalid command data length") + if mrl and mrl > 65536: + raise ValueError("invalid max response length") + if data: + apdu += pack(">xH", len(data)) + bytes(data) + if mrl > 0: + le = 0 if mrl == 65536 else mrl + apdu += pack(">H", le) if data else pack(">xH", le) + + apdu = self.transceive(apdu) + + if not apdu or len(apdu) < 2: + raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR) + + if check_status and apdu[-2:] != b"\x90\x00": + raise Type4TagCommandError.from_status(apdu[-2:]) + + return apdu[:-2] if check_status else apdu + + def __str__(self): + s = "{tag.__class__.__name__} MIU={tag._dep.miu} FWT={tag._dep.fwt:f}" + return s.format(tag=self) + + +class Type4ATag(Type4Tag): + def __init__(self, clf, target): + super(Type4ATag, self).__init__(clf, target) + self._nfcid = bytearray(target.sdd_res) + + log.debug("send RATS command to activate the Type 4A Tag") + if self.clf.max_recv_data_size < 256: + log.warning("{0} does not support fsd 256".format(self.clf)) + rats_cmd = bytearray.fromhex("E0 70") + else: + rats_cmd = bytearray.fromhex("E0 80") + rats_res = self.clf.exchange(rats_cmd, timeout=0.03) + log.debug("rcvd RATS response: {0}".format(hexlify(rats_res).decode())) + + fsci, fwti = rats_res[1] & 0x0F, rats_res[3] >> 4 + if fsci > 8: + log.warning("FSCI with RFU value in RATS_RES") + fsci = 8 + if fwti > 14: + log.warning("FWI with RFU value in RATS_RES") + fwti = 4 + + fsc = (16, 24, 32, 40, 48, 64, 96, 128, 256)[fsci] + fwt = 4096 / 13.56E6 * (2**fwti) + + if fsc > self.clf.max_send_data_size: + log.warning("{0} does not support fsc {1}".format(self.clf, fsc)) + fsc = self.clf.max_send_data_size + + log.debug("max command frame size is {0:d} byte".format(fsc)) + log.debug("max frame waiting time is {0:f}".format(fwt)) + + self._dep = IsoDepInitiator(clf, fsc, fwt) + self._extended_length_support = False + + +class Type4BTag(Type4Tag): + def __init__(self, clf, target): + super(Type4BTag, self).__init__(clf, target) + self._nfcid = bytearray(target.sensb_res[1:5]) + + log.debug("send ATTRIB command to activate the Type 4B Tag") + if self.clf.max_recv_data_size < 256: + log.warning("{0} does not support fsd 256".format(self.clf)) + attrib_cmd = b'\x1D' + self._nfcid + b'\x00\x07\x01\x00' + else: + attrib_cmd = b'\x1D' + self._nfcid + b'\x00\x08\x01\x00' + attrib_res = self.clf.exchange(attrib_cmd, timeout=0.03) + log.debug("rcvd ATTRIB response %s", hexlify(attrib_res).decode()) + + fsci, fwti = target.sensb_res[10] >> 4, target.sensb_res[11] >> 4 + if fsci > 8: + log.warning("FSCI with RFU value in SENSB_RES") + fsci = 8 + if fwti > 14: + log.warning("FWI with RFU value in SENSB_RES") + fwti = 4 + + fsc = (16, 24, 32, 40, 48, 64, 96, 128, 256)[fsci] + fwt = 4096 / 13.56E6 * (2**fwti) + + if fsc > self.clf.max_send_data_size: + log.warning("{0} does not support fsc {1}".format(self.clf, fsc)) + fsc = self.clf.max_send_data_size + + log.debug("max command frame size is {0:d} byte".format(fsc)) + log.debug("max frame waiting time is {0:f}".format(fwt)) + + self._dep = IsoDepInitiator(clf, fsc, fwt) + self._extended_length_support = False + + +def activate(clf, target): + if target.brty.endswith('A'): + return Type4ATag(clf, target) + if target.brty.endswith('B'): + return Type4BTag(clf, target) diff --git a/src/test/rfid.py b/src/test/rfid.py index 14caac2..8a2fa16 100644 --- a/src/test/rfid.py +++ b/src/test/rfid.py @@ -8,8 +8,8 @@ from pathlib import Path import serial import ndef -import nfc -from nfc.clf import RemoteTarget +from src.lib import nfc as nfc +from src.lib.nfc.clf import RemoteTarget logging.basicConfig( format="{asctime}:{name}:{levelname}:{message}",