Skip to main content

A RFID/NFC automation testing framework for cards and readers.

Project description

nfctester 🥕

License Python Version

nfctester 是一个专为 RFID/NFC 卡片与读卡器设计的自动化测试框架。项目采用严谨的分层架构设计,实现了硬件通信、芯片驱动、卡片逻辑与加密算法的深度解耦,旨在为 RFID 技术研究、漏洞分析及产品测试提供一个健壮且易于扩展的基础平台。支持 PN532 和 CLRC663 两种读卡器,通过注册表机制实现无缝切换。

🌟 核心特性

  • 分层架构: 清晰的 9 层体系结构,模块化程度高,易于维护与扩展。
  • 广泛的协议支持:
    • 卡片: Mifare Classic, ISO14443A, NFC Forum Type 2 Tag (NTAG21x/22x 等)。
    • 芯片: 深度优化 PN532 HSU 和 CLRC663 UART 驱动,均支持位帧 (Bit-framing) 收发。
  • 强大加密支持: 内置 AES-128 (CBC)、Mifare Crypto1 算法引擎,支持 NTAG22x AES 互认证。
  • 可视化跟踪: 跟踪控制层与协议解析层,提供结构化的通信日志输出(hex + 摘要标签),完美还原协议交互细节。
  • 协议解析器自动切换: 通过 ParserRegistry 注册 ATQA/SAK → 解析器映射,寻卡时自动切换协议解析器。
  • 插件化扩展: 通过 Registry 模式,外部只需 .py 文件 + 装饰器即可接入自定义读卡器和协议解析器,无需打包。

🏗️ 架构体系 (9-Layer Architecture)

项目遵循高度抽象的设计模式,分为以下九层:

  1. 硬件传输层 (Hardware): 负责底层字节流传输(如 SerialTransport)。
  2. 驱动层 (Driver): 实现特定芯片(如 PN532、CLRC663)的协议封装与寄存器操作。
  3. 注册表与会话 (Registry): 类注册(Transport/CardReader/Card/Parser)、会话管理,贯穿硬件层与驱动层。
  4. 卡片逻辑层 (Card): 定义各种 RFID 标签与智能卡的协议行为(Mifare, NTAG 等)。
  5. 加密算法层 (Crypto): 提供原子级的加密/解密操作(AES, Crypto1)。
  6. 通用工具层 (Utility): 包含 CRC 校验、位操作等基础辅助函数。
  7. 跟踪控制层 (Trace): 中心化的日志管理系统,实现业务逻辑与通信监控的分离。
  8. 协议解析层 (Parsers): 将字节流解析为结构化字段,供格式化输出使用。
  9. 脚本/CLI 层 (CLI): 提供开箱即用的命令行工具(如 aes128-clipn532-scanner)。

🚀 快速上手

环境准备

推荐使用 uv 进行环境管理:

# 克隆仓库
git clone https://github.com/crthu/nfctester.git
cd nfctester

# 同步依赖
uv sync

基本用法:Registry 创建读卡器与卡片

from nfctester.registry import CardReaderRegistry, CardRegistry

# 1. 一行创建读卡器(自动创建 transport 并注入)
reader = CardReaderRegistry.create("pn532", transport="serial", port="COM20")
reader.open()

# 2. 寻卡(自动切换协议解析器)
card_info = reader.active()
if card_info:
    # 假设已知卡片类型为 mifare_classic
    card = CardRegistry.create("mifare_classic", reader=reader)
    print(f"UID: {card_info.uid.hex(' ').upper()}")

reader.close()

Session:上下文管理器(自动 open/close)

from nfctester.registry import session

# 自动管理 reader 的生命周期,类似 C# 的 using
with session("pn532", transport="serial", port="COM20") as s:
    card_info = s.active()
    if card_info:
        res = s.transceive_bits(b"\x26", last_tx_bits=7, tx_crc=False, rx_crc=False)
        if res.data:
            print(f"ATQA: {res.data.hex(' ').upper()}")
# 退出时自动 close

Mifare Classic 认证与读写

with session("pn532", transport="serial", port="COM20") as s:
    card_info = s.active()
    if card_info:
        # 使用 reader 级别的硬件认证,uid 来自 active() 返回的 CardInfo
        if s.mf_auth(block=4, key_type=0x60, key=b'\xff\xff\xff\xff\xff\xff', uid=card_info.uid):
            # 认证后 transceive 自动走加密通道
            res = s.transceive(b'\x30\x04')  # READ block 4
            if res.data:
                print(f"Block 4: {res.data.hex(' ').upper()}")

🔌 自定义读卡器

框架通过 Registry 模式支持外部读卡器扩展。只需继承 CardReader 基类并用装饰器注册即可。

1. 自定义 Transport(可选)

如果你的硬件不是串口,先注册一个 Transport:

from nfctester.registry import TransportRegistry
from nfctester.hardware.base import Transport

@TransportRegistry.register("tcp")
class TCPTransport(Transport):
    def __init__(self, host="127.0.0.1", port=5000):
        import socket
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.connect((host, port))

    def write(self, data: bytes):
        self.sock.sendall(data)

    def read(self, size: int) -> bytes:
        return self.sock.recv(size)

    def flush_input(self):
        self.sock.setblocking(False)
        try:
            while self.sock.recv(4096):
                pass
        except BlockingIOError:
            pass
        finally:
            self.sock.setblocking(True)

    def close(self):
        self.sock.close()

2. 自定义 CardReader

from nfctester.registry import CardReaderRegistry
from nfctester.drivers.card_reader import CardReader, CardInfo, TransceiveBits

@CardReaderRegistry.register("acr122u")
class ACR122UReader(CardReader):
    def __init__(self, transport):
        self.transport = transport

    def open(self):
        self.transport.write(b"\xFF\x00\x00\x00\x00")
        self.transport.flush_input()

    def close(self):
        self.transport.close()

    def get_version(self) -> list[int]:
        self.transport.write(b"\xFF\x00\x48\x00\x00")
        return list(self.transport.read(10))

    @property
    def rf_field(self) -> bool:
        return True

    @rf_field.setter
    def rf_field(self, enabled: bool):
        pass

    def _do_active(self) -> CardInfo | None:
        cmd = b"\xD4\x4A\x01\x00"
        frame = bytes([0xFF, 0x00, 0x00, 0x00, len(cmd)]) + cmd
        self.transport.write(frame)
        res = self.transport.read(20)
        if res and len(res) >= 10:
            return CardInfo(uid=list(res[6:10]), atq=list(res[2:4]), sak=res[4])
        return None

    @property
    def mf_crypto(self) -> bool:
        return False

    def mf_auth(self, block: int, key_type: int, key: list[int], uid: list[int]) -> bool:
        return False

    def transceive(self, data: list[int], last_tx_bits: int = 0, tx_crc: bool = True, rx_crc: bool = True) -> TransceiveBits:
        frame = bytes([0xFF, 0x00, 0x00, 0x00, len(data)]) + bytes(data)
        self.transport.write(frame)
        res = self.transport.read(262)
        return TransceiveBits(data=list(res) if res else [], bits=0)

3. 使用你的自定义读卡器

import my_reader  # import 即自动注册
from nfctester.registry import CardReaderRegistry

reader = CardReaderRegistry.create("acr122u", transport="serial", port="COM3")
reader.open()
card_info = reader.active()
reader.close()

4. 自定义协议解析器 (ParserRegistry)

注册自定义协议解析器,寻卡时自动切换:

from nfctester import ParserRegistry
from nfctester.parsers.base_parser import BaseParser, ParsedFrame

@ParserRegistry.register(atqa=0x1234, sak=0x56, name="My Custom Protocol")
class MyProtocolParser(BaseParser):
    def can_parse(self, data: list[int]) -> bool:
        return len(data) > 0 and data[0] == 0xAA

    def parse(self, data: list[int]) -> ParsedFrame:
        # 解析 TX 命令
        ...

    def parse_rx(self, data: list[int], tx: list[int] | None = None) -> ParsedFrame | None:
        # 解析 RX 响应
        ...

5. 自定义卡片注册 (CardRegistry)

from nfctester.registry import CardRegistry
from nfctester.cards.base_card import BaseCard

@CardRegistry.register("my_custom_card")
class MyCustomCard(BaseCard):
    # 实现 BaseCard 定义的抽象方法
    ...

6. 查看已注册的组件

from nfctester.registry import TransportRegistry, CardReaderRegistry, CardRegistry
from nfctester.parsers.registry import ParserRegistry

print("Transports:", TransportRegistry.list())
print("Readers:", CardReaderRegistry.list())
print("Cards:", CardRegistry.list())
print("Parsers:", ParserRegistry.list())

更多示例见 examples/ 目录。

🛠️ 运行工具

框架内置了多个实用的 CLI 工具:

  • PN532 扫描器:
    uv run pn532-scanner
    
  • AES-128 加密工具:
    uv run aes128-cli -m encrypt -i <hex_data> -k <hex_key>
    

🧪 运行测试

# 运行单元测试(默认,无需硬件)
uv run pytest

# 运行硬件在环测试(需连接读卡器)
uv run pytest -m hil --port COM4 --reader clrc663

# 按卡片类型过滤 HIL 测试
uv run pytest -m "hil and mifare" --port COM4
uv run pytest -m "hil and ntag224" --port COM20 --reader pn532

# 运行全部测试(单元 + HIL)
uv run pytest -m ""

📄 开源协议

本项目基于 Apache License 2.0 协议开源。详见 LICENSE 文件。

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nfctester-0.0.47.tar.gz (46.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nfctester-0.0.47-py3-none-any.whl (57.2 kB view details)

Uploaded Python 3

File details

Details for the file nfctester-0.0.47.tar.gz.

File metadata

  • Download URL: nfctester-0.0.47.tar.gz
  • Upload date:
  • Size: 46.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for nfctester-0.0.47.tar.gz
Algorithm Hash digest
SHA256 8fc8e7ccb49722ffbe6c4df5dc2b70571c6155d0d7a89c97ed799b45a0b2975c
MD5 c86b56bc807ecc133ffdb2e04c09a373
BLAKE2b-256 7b81fc9cc2809158459cf77c5a028a7abac403f2e86b68c7d268407aa70996f0

See more details on using hashes here.

Provenance

The following attestation bundles were made for nfctester-0.0.47.tar.gz:

Publisher: publish-pypi.yml on CRThu/nfctester

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nfctester-0.0.47-py3-none-any.whl.

File metadata

  • Download URL: nfctester-0.0.47-py3-none-any.whl
  • Upload date:
  • Size: 57.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for nfctester-0.0.47-py3-none-any.whl
Algorithm Hash digest
SHA256 d630027885cb1b546212674381454cb6d2a641ee51443b1fbe3f044ce12b8a5d
MD5 668aacd769593fea4b4eaebb2fc7fc23
BLAKE2b-256 034a09ea5c8799e0c8d84c53e599fae0a1a7af3e01c66020f5f2a5b1654e54dc

See more details on using hashes here.

Provenance

The following attestation bundles were made for nfctester-0.0.47-py3-none-any.whl:

Publisher: publish-pypi.yml on CRThu/nfctester

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page