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>
314 lines
12 KiB
Python
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"]
|