Files
pm3py/tests/test_sim_mifare.py
michael c1ab980036 refactor: remove all backward-compat shims from sim/
Deletes 24 shim files from sim/. All test imports now point directly
to canonical transponders/ and trace/ paths. sim/ contains only
infrastructure (15 files): frame, memory, transponder ABC, reader ABC,
medium, sim_session, table_compiler, replay, relay, fuzzer, pm3medium,
mcu_bridge, mcu_protocol, dual_session, __init__.

751 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:56:40 -07:00

314 lines
12 KiB
Python

"""Tests for pm3py.sim.crypto1 and pm3py.sim.mifare — Crypto-1 cipher and MIFARE Classic."""
import asyncio
import struct
import pytest
from pm3py.sim.frame import RFFrame
from pm3py.sim.medium import SoftwareMedium
from pm3py.transponders.hf.iso14443a.nxp.crypto1 import Crypto1
from pm3py.transponders.hf.iso14443a.nxp.mifare_classic import MifareClassicTag, MifareClassicReader
from pm3py.transponders.hf.iso14443a.base import Reader14443A, REQA, CL1, NVB_SELECT, _compute_bcc
def run(coro):
return asyncio.get_event_loop().run_until_complete(coro)
# ---------------------------------------------------------------------------
# Crypto-1 LFSR
# ---------------------------------------------------------------------------
class TestCrypto1LFSR:
"""Test Crypto-1 cipher against known test vectors."""
def test_key_load(self):
"""Verify LFSR state after loading a key."""
c = Crypto1(b"\xFF\xFF\xFF\xFF\xFF\xFF")
# After loading all-ones key, LFSR should be non-zero
assert c._lfsr != 0
def test_generate_keystream_is_deterministic(self):
"""Same key produces same keystream."""
c1 = Crypto1(b"\xA0\xA1\xA2\xA3\xA4\xA5")
c2 = Crypto1(b"\xA0\xA1\xA2\xA3\xA4\xA5")
ks1 = [c1.generate_bit() for _ in range(32)]
ks2 = [c2.generate_bit() for _ in range(32)]
assert ks1 == ks2
def test_different_keys_different_keystream(self):
c1 = Crypto1(b"\x00\x00\x00\x00\x00\x00")
c2 = Crypto1(b"\xFF\xFF\xFF\xFF\xFF\xFF")
ks1 = [c1.generate_bit() for _ in range(32)]
ks2 = [c2.generate_bit() for _ in range(32)]
assert ks1 != ks2
def test_encrypt_decrypt_roundtrip(self):
"""Encrypting then decrypting with same state should return original."""
key = b"\xA0\xA1\xA2\xA3\xA4\xA5"
plaintext = b"\xDE\xAD\xBE\xEF"
c_enc = Crypto1(key)
encrypted = c_enc.encrypt_bytes(plaintext)
c_dec = Crypto1(key)
decrypted = c_dec.encrypt_bytes(encrypted) # XOR cipher: enc == dec
assert decrypted == plaintext
def test_known_vector_lfsr_feedback(self):
"""Verify the LFSR feedback polynomial is correct.
The Crypto-1 LFSR polynomial is:
x^48 + x^43 + x^39 + x^38 + x^36 + x^34 + x^33 + x^31 + x^29 +
x^24 + x^23 + x^21 + x^19 + x^13 + x^9 + x^7 + x^6 + x^5 + 1
"""
c = Crypto1(b"\x00\x00\x00\x00\x00\x01")
# Just verify it doesn't crash and produces deterministic output
bits = [c.generate_bit() for _ in range(48)]
assert len(bits) == 48
class TestCrypto1Auth:
"""Test Crypto-1 mutual authentication protocol."""
def test_tag_nonce_generation(self):
"""Tag generates a 4-byte nonce."""
c = Crypto1(b"\xFF\xFF\xFF\xFF\xFF\xFF")
nt = c.generate_nonce()
assert len(nt) == 4
def test_auth_mutual_success(self):
"""Full mutual authentication between tag and reader Crypto-1 instances."""
key = b"\xFF\xFF\xFF\xFF\xFF\xFF"
uid = b"\x01\x02\x03\x04"
# Tag side: generate nonce
tag_crypto = Crypto1(key)
nt = tag_crypto.generate_nonce()
# Initialize both sides with uid ^ nt
uid_int = struct.unpack(">I", uid)[0]
nt_int = struct.unpack(">I", nt)[0]
tag_crypto = Crypto1(key)
tag_crypto.init_auth(uid_int, nt_int)
reader_crypto = Crypto1(key)
reader_crypto.init_auth(uid_int, nt_int)
# Reader generates nr (random) and computes ar = suc(nt, 64)
nr = b"\xAB\xCD\xEF\x01"
# Encrypt nr with reader's keystream
nr_enc = reader_crypto.encrypt_bytes(nr)
# Reader computes ar (encrypted successor of nt)
ar = reader_crypto.encrypt_bytes(struct.pack(">I", _suc(nt_int, 64)))
# Tag decrypts nr
nr_dec = tag_crypto.encrypt_bytes(nr_enc)
assert nr_dec == nr
# Tag decrypts ar and verifies
ar_dec_int = struct.unpack(">I", tag_crypto.encrypt_bytes(ar))[0]
assert ar_dec_int == _suc(nt_int, 64)
# Tag sends at (encrypted successor of nt, 96)
at = tag_crypto.encrypt_bytes(struct.pack(">I", _suc(nt_int, 96)))
# Reader verifies at
at_dec_int = struct.unpack(">I", reader_crypto.encrypt_bytes(at))[0]
assert at_dec_int == _suc(nt_int, 96)
def _suc(nt: int, n: int) -> int:
"""Compute successor of nt by n LFSR clocks (simplified for test)."""
# In real Crypto-1, suc is the LFSR state after n clocks
# For testing, we use a simple PRNG-like computation
val = nt
for _ in range(n):
bit = ((val >> 31) ^ (val >> 20) ^ (val >> 15) ^ (val >> 0)) & 1
val = ((val << 1) | bit) & 0xFFFFFFFF
return val
# ---------------------------------------------------------------------------
# MifareClassicTag — transponder model
# ---------------------------------------------------------------------------
class TestMifareClassicTagBasics:
"""Test MifareClassicTag construction and 14443-A compliance."""
def test_1k_atqa_sak(self):
tag = MifareClassicTag(uid=b"\x01\x02\x03\x04", size="1k")
run(tag.power_on())
resp = run(tag.handle_frame(RFFrame.from_hex("26")))
assert resp.data == b"\x04\x00" # ATQA for 1K
# SELECT
uid = b"\x01\x02\x03\x04"
bcc = _compute_bcc(uid)
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + uid + bytes([bcc]))))
assert resp.data[0] == 0x08 # SAK for 1K
def test_4k_atqa_sak(self):
tag = MifareClassicTag(uid=b"\x01\x02\x03\x04", size="4k")
run(tag.power_on())
resp = run(tag.handle_frame(RFFrame.from_hex("26")))
assert resp.data == b"\x02\x00" # ATQA for 4K
uid = b"\x01\x02\x03\x04"
bcc = _compute_bcc(uid)
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + uid + bytes([bcc]))))
assert resp.data[0] == 0x18 # SAK for 4K
def test_rejects_rats(self):
"""MIFARE Classic is Part 3 only."""
tag = MifareClassicTag(uid=b"\x01\x02\x03\x04", size="1k")
run(tag.power_on())
run(tag.handle_frame(RFFrame.from_hex("26")))
uid = b"\x01\x02\x03\x04"
bcc = _compute_bcc(uid)
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + uid + bytes([bcc]))))
resp = run(tag.handle_frame(RFFrame.from_hex("E050")))
assert resp is None
def test_default_memory_all_zeros(self):
tag = MifareClassicTag(uid=b"\x01\x02\x03\x04", size="1k")
assert len(tag._data) == 1024 # 64 blocks * 16 bytes
assert tag._data[:16] == b"\x00" * 16
def test_4k_memory_size(self):
tag = MifareClassicTag(uid=b"\x01\x02\x03\x04", size="4k")
assert len(tag._data) == 4096 # 256 blocks * 16 bytes
def test_default_keys_are_ff(self):
tag = MifareClassicTag(uid=b"\x01\x02\x03\x04")
for sector in range(16):
assert tag._keys_a[sector] == b"\xFF" * 6
assert tag._keys_b[sector] == b"\xFF" * 6
class TestMifareClassicTagAuth:
"""Test MIFARE Classic authentication."""
def test_auth_command_returns_tag_nonce(self):
"""AUTH command (0x60/0x61) should return a 4-byte encrypted tag nonce."""
tag = MifareClassicTag(uid=b"\x01\x02\x03\x04", size="1k")
run(tag.power_on())
run(tag.handle_frame(RFFrame.from_hex("26")))
uid = b"\x01\x02\x03\x04"
bcc = _compute_bcc(uid)
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + uid + bytes([bcc]))))
# AUTH_A for block 0
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x60\x00")))
assert resp is not None
assert len(resp.data) == 4 # tag nonce (nt)
def test_unauthenticated_read_rejected(self):
"""READ without authentication should be rejected."""
tag = MifareClassicTag(uid=b"\x01\x02\x03\x04", size="1k")
run(tag.power_on())
run(tag.handle_frame(RFFrame.from_hex("26")))
uid = b"\x01\x02\x03\x04"
bcc = _compute_bcc(uid)
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + uid + bytes([bcc]))))
# READ block 0 without auth
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x00")))
assert resp is None # rejected
class TestMifareClassicTagMemory:
"""Test MIFARE Classic memory layout."""
def test_1k_sector_block_mapping(self):
"""1K: sectors 0-15, 4 blocks each."""
tag = MifareClassicTag(uid=b"\x01\x02\x03\x04", size="1k")
assert tag.sector_for_block(0) == 0
assert tag.sector_for_block(3) == 0
assert tag.sector_for_block(4) == 1
assert tag.sector_for_block(63) == 15
def test_4k_sector_block_mapping(self):
"""4K: sectors 0-31 = 4 blocks, sectors 32-39 = 16 blocks."""
tag = MifareClassicTag(uid=b"\x01\x02\x03\x04", size="4k")
assert tag.sector_for_block(0) == 0
assert tag.sector_for_block(127) == 31 # last block of sector 31
assert tag.sector_for_block(128) == 32 # first block of sector 32
assert tag.sector_for_block(255) == 39 # last block of sector 39
def test_read_block_data(self):
"""Direct read of block data from memory."""
data = bytearray(1024)
data[0:16] = b"\xDE\xAD\xBE\xEF" + b"\x00" * 12
tag = MifareClassicTag(uid=b"\x01\x02\x03\x04", size="1k", data=data)
assert tag.read_block_raw(0) == b"\xDE\xAD\xBE\xEF" + b"\x00" * 12
def test_write_block_data(self):
"""Direct write of block data to memory."""
tag = MifareClassicTag(uid=b"\x01\x02\x03\x04", size="1k")
new_data = b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10"
tag.write_block_raw(4, new_data)
assert tag.read_block_raw(4) == new_data
class TestMifareClassicReader:
"""Test MifareClassicReader with full auth + read/write."""
def test_read_block_with_default_key(self):
"""Reader authenticates and reads a block."""
medium = SoftwareMedium()
data = bytearray(1024)
data[16:32] = b"\xCA\xFE\xBA\xBE" + b"\x00" * 12
tag = MifareClassicTag(uid=b"\x01\x02\x03\x04", size="1k", data=data)
run(medium.attach(tag))
reader = MifareClassicReader(medium)
result = run(reader.read_block(
uid=b"\x01\x02\x03\x04",
block=1,
key=b"\xFF\xFF\xFF\xFF\xFF\xFF",
))
assert result["success"]
assert result["data"][:4] == b"\xCA\xFE\xBA\xBE"
def test_write_block_with_default_key(self):
"""Reader authenticates and writes a block."""
medium = SoftwareMedium()
tag = MifareClassicTag(uid=b"\x01\x02\x03\x04", size="1k")
run(medium.attach(tag))
reader = MifareClassicReader(medium)
new_data = b"\x01" * 16
result = run(reader.write_block(
uid=b"\x01\x02\x03\x04",
block=4,
data=new_data,
key=b"\xFF\xFF\xFF\xFF\xFF\xFF",
))
assert result["success"]
# Verify by reading back
result = run(reader.read_block(
uid=b"\x01\x02\x03\x04",
block=4,
key=b"\xFF\xFF\xFF\xFF\xFF\xFF",
))
assert result["data"] == new_data
def test_wrong_key_fails(self):
"""Reader with wrong key cannot authenticate."""
medium = SoftwareMedium()
tag = MifareClassicTag(uid=b"\x01\x02\x03\x04", size="1k")
run(medium.attach(tag))
reader = MifareClassicReader(medium)
result = run(reader.read_block(
uid=b"\x01\x02\x03\x04",
block=0,
key=b"\x00\x00\x00\x00\x00\x00", # wrong key
))
assert not result["success"]