Skip to main content

Python

NFC Writer / Reader

Not so long time ago, like last week 20.03.2024 I needed to write a lot of NTAG215 NFC cards. I needed to write an URL on to them. But not only that, I needed also lock them with password so not just anybody could write to them again.

Here I sit with box of 500 cards and could not found software to do it ! I mean I had one, nfctools which is fantastic, but not automatic. It require quite a lot of clicks and typing to write what I need and lock the card...

Python to the rescue ! The first result from google come as nfcpy and after reading the documentation, I was like, great ! this will be easy.... well it was not. The library supports only few reader/writers and as luck will have it... I had the one unsupported and since the author gave up on the library it also won't be supported... I do not bad mouth the author, in no way, I now fully understand why he gave up on the library, more on this later.

🚩
My reader/writer is ACR1252

The next library I found was pyscard thanks the silicon gods this one works. But there is a catch, this is quite low level communication with the reader and without intimate knowledge of your specific NFC chip how it work, and what the hell is NDEF protocol you will not do much. And I mean intimate you need to understand how the NFC tag is structured, what memory banks are for data etc... fun times...

The author of nfcpy most likely gave up cause you need to reliably detect correct chip witch is not that easy, and basically deal with low level access to that specific format/chip. And the second kick in the balls is, that various NFC readers/writers have various API them self, some commands are universal and some are not... its a nightmare ! I could not even get most of the API commands to work with my reader, and I had a manual to it... so that is that...

After about a weak and some change, I can confidently tell I now understand NTAG215 format and wish I did not need to.

Code

Enough BS, here is some code. My goal was to get the reader to "listen" for NTAG215 and when detected write URL in NDEF format to it. NDEF so mobile device can recognize it as URL. Then lock it with password, which is different to changing locking bit that would permanently and irreversibly lock the card... Yes I have killed 3 cards this way. So it will write protect the card with password and this can be reversed.

😵‍💫
I seriously could not find working example on internet, GitHub or anywhere else. As far as I know this is only working example 🖥️ python code writing to NFC tag with ACR1252

GitHub link with full code: https://github.com/VladoPortos/python-nfc-read-write-acr1252

Write

Bellow code expect two variables and that is the URL to write and password.

Password: careful with this, cause basically every program have own implementation of the password locking, since you can't really use whole passphrase, locking the cards with this script will not allow unlocking in other software with the same passphrase. I have included authentication function and function to remove the password permanently. Basically returning the cards to default.


from typing import Dict, List
from dotenv import load_dotenv
from smartcard.CardMonitoring import CardMonitor, CardObserver
from smartcard.util import toHexString
from smartcard.CardConnection import CardConnection
from smartcard.System import *
import hashlib
import os
import ndef

# Load environment variables from .env file
load_dotenv()
# Access variables
NFC_URL: str = os.getenv('NFC_URL', 'https://some-random-url.com')
PASSPHRASE: str = os.getenv('NFC_PASSPHRASE', 'YourSecurePassphrase')




def decode_atr(atr: str) -> Dict[str, str]:
    """Decode the ATR (Answer to Reset) string into readable components.

    Args:
        atr (str): ATR string.

    Returns:
        Dict[str, str]: Dictionary containing readable information about the card.
    """
    atr = atr.split(" ")

    rid = atr[7:12]
    standard = atr[12]
    card_name = atr[13:15]

    card_names = {
        "00 01": "MIFARE Classic 1K",
        "00 38": "MIFARE Plus® SL2 2K",
        "00 02": "MIFARE Classic 4K",
        "00 39": "MIFARE Plus® SL2 4K",
        "00 03": "MIFARE Ultralight®",
        "00 30": "Topaz and Jewel",
        "00 26": "MIFARE Mini®",
        "00 3B": "FeliCa",
        "00 3A": "MIFARE Ultralight® C",
        "FF 28": "JCOP 30",
        "00 36": "MIFARE Plus® SL1 2K",
        "FF[SAK]": "undefined tags",
        "00 37": "MIFARE Plus® SL1 4K",
        "00 07": "SRIX"
    }

    standards = {
        "03": "ISO 14443A, Part 3",
        "11": "FeliCa"
    }

    return {
        "RID": " ".join(rid),
        "Standard": standards.get(standard, "Unknown"),
        "Card Name": card_names.get(" ".join(card_name), "Unknown")
    }


def authenticate_with_password(connection: CardConnection, passphrase: str) -> bool:
    """Authenticate with the NTAG215 NFC tag using the provided passphrase.

    Args:
        connection (CardConnection): Connection to the card.
        passphrase (str): Passphrase for authentication.

    Returns:
        bool: True if authentication is successful, otherwise False.
    """
    password = derive_password(passphrase)

    command = [0xFF, 0x00, 0x00, 0x00, 0x07, 0xD4, 0x42, 0x1B] + password
    response, sw1, sw2 = connection.transmit(command)

    # print(f"Command being sent for authentication: {' '.join(f'{byte:02X}' for byte in command)}")
    # print(f"Response: {' '.join(f'{byte:02X}' for byte in response)}, SW1: {sw1:02X}, SW2: {sw2:02X}")

    if sw1 == 0x90 and sw2 == 0x00:
        # Check if the PACK is part of the response and correctly positioned
        if len(response) >= 5:  # Ensuring the response is long enough
            # Adjust this based on where PACK actually appears
            pack = response[3:5]
            print("Authentication successful, PACK received:",
                  ' '.join(f'{byte:02X}' for byte in pack))
            return True
        else:
            print("PACK not received or incorrectly formatted")
    else:
        print("Authentication failed")
    return False


def create_ndef_record(url: str) -> bytes:
    """Encodes a given URI into a complete NDEF message using ndeflib.

    Args:
        url (str): The URI to be encoded into an NDEF message.

    Returns:
        bytes: The complete NDEF message as bytes, ready to be written to an NFC tag.
    """

    uri_record = ndef.UriRecord(url)

    # Encode the NDEF message
    encoded_message = b''.join(ndef.message_encoder([uri_record]))

    # Calculate total length of the NDEF message (excluding start byte and terminator)
    message_length = len(encoded_message)

    # Create the initial part of the message with start byte, length, encoded message, and terminator
    initial_message = b'\x03' + message_length.to_bytes(1, 'big') + encoded_message + b'\xFE'

    # Calculate padding to align to the nearest block size (assuming 4 bytes per block)
    padding_length = -len(initial_message) % 4
    complete_message = initial_message + (b'\x00' * padding_length)
    return complete_message


def write_ndef_message(connection: CardConnection, ndef_message: bytes) -> bool:
    """Writes the NDEF message to the NFC tag.

    Args:
        connection (CardConnection): The connection to the NFC tag.
        ndef_message (bytes): The NDEF message to be written.

    Returns:
        bool: True if the write operation is successful, False otherwise.
    """
    page = 4
    while ndef_message:
        block_data = ndef_message[:4]
        ndef_message = ndef_message[4:]
        WRITE_COMMAND = [0xFF, 0xD6, 0x00, page, 0x04] + list(block_data)
        response, sw1, sw2 = connection.transmit(WRITE_COMMAND)
        if sw1 != 0x90 or sw2 != 0x00:
            print(f"Failed to write to page {
                  page}, SW1: {sw1:02X}, SW2: {sw2:02X}")
            return False
        print(f"Successfully wrote to page {page}")
        page += 1
    return True


def derive_password(passphrase: str) -> List[int]:
    """Hash passphrase and return first 4 bytes as password for NFC tag authentication.

    Args:
        passphrase (str): Passphrase to hash.

    Returns:
        List[int]: List of the first 4 bytes of the hash.
    """
    hasher = hashlib.sha256()
    hasher.update(passphrase.encode())
    # print(f"Hashed passphrase: {hasher.hexdigest()}")
    # print(f"Password: {list(hasher.digest()[:4])}")
    return list(hasher.digest()[:4])


def set_password(connection: CardConnection, passphrase: str) -> None:
    """
    Set password protection on an NTAG215 NFC tag.

    Args:
        connection (CardConnection): The connection to the NFC tag.
        passphrase (str): The passphrase used to derive the password.
    """
    # Addr 82: LOCK2 - LOCK4
    # Addr 83: CFG 0 (MIROR/AUTH0)
    # Addr 84: CFG 1 (ACCESS)
    # Addr 85: PWD0 - PWD3
    # Addr 86: PACK0 - PACK1

    password = derive_password(passphrase)

    pack = [0x00, 0x00]  # Example PACK, adjust as needed

    # Write password to page 0x85
    connection.transmit([0xFF, 0xD6, 0x00, 0x85, 0x04] + password)

    # Write PACK to page 0x86
    connection.transmit([0xFF, 0xD6, 0x00, 0x86, 0x04] + pack)

    # Set AUTH0 to enable password protection from a specific page
    # Example: Protect from page 4 onwards
    # AUTH0 is at page 0x83, last byte of the 4-byte page
    auth0_command = [0xFF, 0xD6, 0x00, 0x83, 0x04, 0x00, 0x00, 0x00, 0x04]
    connection.transmit(auth0_command)

    # Set ACCESS configuration
    # Example: Enable both read and write protection
    # ACCESS is at page 0x84, typically a single-byte configuration
    # Adjust 0x80 as needed based on datasheet
    access_command = [0xFF, 0xD6, 0x00, 0x84, 0x01, 0x80]
    connection.transmit(access_command)

    print("Password and protection configuration set.")


def remove_password(connection: CardConnection, passphrase: str) -> None:
    """
    Remove password protection from an NTAG215 NFC tag.

    Args:
        connection (CardConnection): The connection to the NFC tag.
        passphrase (str): The passphrase used to derive the password.
    """
    password = derive_password(passphrase)

    # Authenticate with the password first
    # Assuming that the tag requires password authentication for writing
    connection.transmit([0xFF, 0x00, 0x00, 0x00] + password + [0x00])

    # Disable password protection by setting AUTH0 beyond the tag's storage
    # Example: set AUTH0 to 0xFF to disable all protections
    disable_auth0_command = [0xFF, 0xD6, 0x00,
                             0x83, 0x04, 0x00, 0x00, 0x00, 0xFF]
    response, sw1, sw2 = connection.transmit(disable_auth0_command)
    if sw1 == 0x90 and sw2 == 0x00:
        print("Password protection successfully removed.")
    else:
        print(f"Failed to remove password protection, SW1: {sw1}, SW2: {sw2}")


def is_password_set(connection: CardConnection) -> bool:
    """
    Check if a password is set on an NTAG215 tag by reading the AUTH0 register.

    Args:
        connection (CardConnection): The connection to the NFC tag.

    Returns:
        bool: True if a password is set (AUTH0 not 0xFF), otherwise False.
    """
    # Address 0x83 is used for AUTH0 in NTAG215
    # Convert page address to byte address if necessary
    page_address = 0x83
    # APDU for reading one page (4 bytes)
    read_command = [0xFF, 0xB0, 0x00, page_address, 0x04]

    try:
        response, sw1, sw2 = connection.transmit(read_command)
        # print(f"Read AUTH0, Response: {' '.join(f'{byte:02X}' for byte in response)}, SW1: {sw1:02X}, SW2: {sw2:02X}")

        if sw1 == 0x90 and sw2 == 0x00:
            # Response is expected to be 4 bytes, AUTH0 is the last byte
            auth0 = response[3]
            # print(f"AUTH0 register value: {auth0:02X}")

            # Check if AUTH0 is 0xFF for no password protection
            if auth0 == 0xFF:
                # print("No password protection is set.")
                return False
            else:
                # print("Password protection is set.")
                return True
        else:
            print("Failed to read AUTH0 register.")
            return False

    except Exception as e:
        print(f"An error occurred: {e}")
        return False


class NTAG215Observer(CardObserver):
    """Observer class for NFC card detection and processing."""

    def update(self, observable, actions):
        global cards_processed
        (addedcards, _) = actions
        for card in addedcards:
            print(f"Card detected, ATR: {toHexString(card.atr)}")
            try:
                connection = card.createConnection()
                connection.connect()
                print("Connected to card")

                # if password is set, authenticate
                if is_password_set(connection):
                    authenticate_with_password(connection, PASSPHRASE)

                # Write NDEF message to tag
                write_ndef_message(connection, create_ndef_record(NFC_URL))

                # if password is not set, set password
                if not is_password_set(connection):
                    set_password(connection, PASSPHRASE)

                # Get card information
                # info = decode_atr(toHexString(card.atr))
                # print(f"Card Name: {info['Card Name']}, Standard: {
                #       info['Standard']}, RID: {info['RID']}")

                # # Get card UID
                # SELECT = [0xFF, 0xCA, 0x00, 0x00, 0x00]
                # response, sw1, sw2 = connection.transmit(SELECT)
                # uid = toHexString(response)
                # print(f"Card UID: {uid}")

                # Authenticate with the password
                # authenticate_with_password(connection, PASSPHRASE)
                # Remove password protection from the tag
                # remove_password(connection, PASSPHRASE)

                cards_processed += 1
                print(f"Total cards flashed: {cards_processed}")

            except Exception as e:
                print(f"An error occurred: {e}")


def main():
    print("Starting NFC card processing...")
    cardmonitor = CardMonitor()
    cardobserver = NTAG215Observer()
    cardmonitor.addObserver(cardobserver)

    try:
        input("Press Enter to stop...\n")
    finally:
        cardmonitor.deleteObserver(cardobserver)
        print(f"Stopped NFC card processing. Total cards processed: {
              cards_processed}")


if __name__ == "__main__":
    # get and print a list of readers attached to the system
    # sc_readers = readers()
    # print(sc_readers)
    cards_processed: int = 0
    main()

Read

This is an checking script, this one reads the NTAG 215 and compare the written NDEF message with the one set as variable. Basically I used it to check if all cards have what they should have.

from smartcard.CardMonitoring import CardMonitor, CardObserver
from smartcard.util import toHexString
from smartcard.CardConnection import CardConnection
import os
import ndef
from dotenv import load_dotenv
from preferredsoundplayer import *

# Load environment variables from .env file
load_dotenv()
# Access variables
NFC_URL: str = os.getenv('NFC_URL', 'https://some-random-url.com')
PASSPHRASE: str = os.getenv('NFC_PASSPHRASE', 'YourSecurePassphrase')


def read_ndef_message(connection: CardConnection, expected_message: bytes) -> bool:
    """Reads the NDEF message from the NFC tag and compares it to the expected message."""
    print("Expected NDEF message:", expected_message.hex())
    # Start reading from the expected starting page of NDEF message
    read_command = [0xFF, 0xB0, 0x00, 4, 0x04]
    message = b''
    try:
        while True:  # Loop to read all parts of the NDEF message
            response, sw1, sw2 = connection.transmit(read_command)
            if sw1 == 0x90 and sw2 == 0x00:
                message += bytes(response[:4])  # Append only the NDEF data
                # print(f"Read data from page {read_command[3]}: {bytes(response[:4]).hex()}")
                if 0xFE in response:  # Look for end byte of NDEF message within the response
                    break
                read_command[3] += 1  # Move to the next page
            else:
                print(f"Failed to read at page {
                      read_command[3]}: SW1={sw1:02X}, SW2={sw2:02X}")
                return False

        print("Read NDEF message:", message.hex())
        if message == expected_message:
            print("Verification successful: Data matches the expected NDEF message.")
            return True
        else:
            print("Verification failed: Data does not match the expected NDEF message.")
            return False
    except Exception as e:
        print(f"Error during reading: {e}")
        return False


def beep(success: bool) -> None:
    """
    Plays a sound based on the success status.

    Args:
        success (bool): Indicates whether the operation was successful or not.

    Returns:
        None
    """
    if success:
        soundplay('ok.wav')
    else:
        soundplay('error.wav')


def create_ndef_record(url: str) -> bytes:
    """Encodes a given URI into a complete NDEF message using ndeflib.

    Args:
        url (str): The URI to be encoded into an NDEF message.

    Returns:
        bytes: The complete NDEF message as bytes, ready to be written to an NFC tag.
    """

    uri_record = ndef.UriRecord(url)

    # Encode the NDEF message
    encoded_message = b''.join(ndef.message_encoder([uri_record]))

    # Calculate total length of the NDEF message (excluding start byte and terminator)
    message_length = len(encoded_message)

    # Create the initial part of the message with start byte, length, encoded message, and terminator
    initial_message = b'\x03' + \
        message_length.to_bytes(1, 'big') + encoded_message + b'\xFE'

    # Calculate padding to align to the nearest block size (assuming 4 bytes per block)
    padding_length = -len(initial_message) % 4
    complete_message = initial_message + (b'\x00' * padding_length)
    return complete_message

class NTAG215Observer(CardObserver):
    """Observer class for NFC card detection and processing."""

    def update(self, observable, actions):
        global cards_processed
        (addedcards, _) = actions
        for card in addedcards:
            print(f"Card detected, ATR: {toHexString(card.atr)}")
            try:
                connection = card.createConnection()
                connection.connect()
                print("Connected to card")

                expected_ndef_message = create_ndef_record(NFC_URL)

                if read_ndef_message(connection, expected_ndef_message):
                    beep(True)  # On success
                else:
                    beep(False)  # On failure

                cards_processed += 1
                print(f"Total cards processed: {cards_processed}")

            except Exception as e:
                print(f"An error occurred: {e}")


def main():
    print("Starting NFC card processing...")
    cardmonitor = CardMonitor()
    cardobserver = NTAG215Observer()
    cardmonitor.addObserver(cardobserver)

    try:
        input("Press Enter to stop...\n")
    finally:
        cardmonitor.deleteObserver(cardobserver)
        print("Stopped NFC card processing. Total cards processed:", cards_processed)


if __name__ == "__main__":
    cards_processed = 0
    main()

Next, I'm working on 3D printed machine that can load cards into hopper, pass it over reader/writer and dump them to some box... 😄

Here are some PDFs I found mildly useful during research.