"""Tests for pm3py.sim.desfire — DESFire EV1/EV2 transponder and reader.""" import asyncio import pytest from pm3py.sim.frame import RFFrame from pm3py.sim.medium import SoftwareMedium from pm3py.transponders.hf.iso14443a.base import _compute_bcc from pm3py.transponders.hf.iso14443a.nxp.desfire import DesfireTag, DesfireReader def run(coro): return asyncio.get_event_loop().run_until_complete(coro) # --------------------------------------------------------------------------- # DesfireTag — basics # --------------------------------------------------------------------------- class TestDesfireTagBasics: def test_sak_indicates_isodep(self): tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06") run(tag.power_on()) resp = run(tag.handle_frame(RFFrame.from_hex("26"))) assert resp.data == b"\x44\x03" # DESFire ATQA # SELECT (7-byte UID) ct_uid = b"\x88" + tag._uid[0:3] bcc1 = _compute_bcc(ct_uid) run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct_uid + bytes([bcc1])))) cl2_uid = tag._uid[3:7] bcc2 = _compute_bcc(cl2_uid) resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + cl2_uid + bytes([bcc2])))) assert resp.data[0] & 0x20 != 0 # SAK bit 5 = ISO-DEP def test_rats_returns_ats(self): tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06") run(tag.power_on()) self._select(tag) resp = run(tag.handle_frame(RFFrame.from_hex("E050"))) assert resp is not None assert tag.state == "PROTOCOL" def _select(self, tag): run(tag.handle_frame(RFFrame.from_hex("26"))) ct_uid = b"\x88" + tag._uid[0:3] bcc1 = _compute_bcc(ct_uid) run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct_uid + bytes([bcc1])))) cl2_uid = tag._uid[3:7] bcc2 = _compute_bcc(cl2_uid) run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + cl2_uid + bytes([bcc2])))) class TestDesfireTagApplications: """Test DESFire application/file structure.""" def test_get_version(self): tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06") self._activate(tag) resp = self._send_desfire(tag, b"\x60") # GetVersion assert resp is not None assert resp[0] == 0xAF # AF = additional frames follow def test_get_application_ids_default(self): """Default tag has only PICC application (0x000000).""" tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06") self._activate(tag) resp = self._send_desfire(tag, b"\x6A") # GetApplicationIDs assert resp is not None assert resp[0] == 0x00 # OK def test_create_application(self): tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06") self._activate(tag) # CreateApplication: AID=0x010203, key_settings=0x0F, num_keys=1 resp = self._send_desfire(tag, b"\xCA\x01\x02\x03\x0F\x01") assert resp is not None assert resp[0] == 0x00 # OK def test_select_application(self): tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06") self._activate(tag) # Create app self._send_desfire(tag, b"\xCA\x01\x02\x03\x0F\x01") # Select app resp = self._send_desfire(tag, b"\x5A\x01\x02\x03") assert resp is not None assert resp[0] == 0x00 def test_create_and_read_standard_file(self): tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06") self._activate(tag) self._send_desfire(tag, b"\xCA\x01\x02\x03\x0F\x01") # CreateApp self._send_desfire(tag, b"\x5A\x01\x02\x03") # SelectApp # CreateStdDataFile: file_no=0, comm_settings=0, access_rights=0xEEEE, size=16 resp = self._send_desfire(tag, b"\xCD\x00\x00\xEE\xEE\x10\x00\x00") assert resp[0] == 0x00 # WriteData: file_no=0, offset=0, length=4, data resp = self._send_desfire(tag, b"\x3D\x00\x00\x00\x00\x04\x00\x00\xDE\xAD\xBE\xEF") assert resp[0] == 0x00 # ReadData: file_no=0, offset=0, length=4 resp = self._send_desfire(tag, b"\xBD\x00\x00\x00\x00\x04\x00\x00") assert resp[0] == 0x00 assert resp[1:5] == b"\xDE\xAD\xBE\xEF" def test_delete_application(self): tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06") self._activate(tag) self._send_desfire(tag, b"\xCA\x01\x02\x03\x0F\x01") resp = self._send_desfire(tag, b"\xDA\x01\x02\x03") # DeleteApplication assert resp[0] == 0x00 def test_authenticate_default_key(self): """Authenticate with default all-zero AES key.""" tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06") self._activate(tag) self._send_desfire(tag, b"\xCA\x01\x02\x03\x0F\x81") # CreateApp, AES keys self._send_desfire(tag, b"\x5A\x01\x02\x03") # AuthenticateAES: key_no=0 resp = self._send_desfire(tag, b"\xAA\x00") assert resp is not None # Response: status + encrypted challenge (16 bytes for AES) assert resp[0] == 0xAF # AF = additional frame expected def _activate(self, tag): run(tag.power_on()) run(tag.handle_frame(RFFrame.from_hex("26"))) ct_uid = b"\x88" + tag._uid[0:3] bcc1 = _compute_bcc(ct_uid) run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct_uid + bytes([bcc1])))) cl2_uid = tag._uid[3:7] bcc2 = _compute_bcc(cl2_uid) run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + cl2_uid + bytes([bcc2])))) run(tag.handle_frame(RFFrame.from_hex("E050"))) # RATS def _send_desfire(self, tag, cmd: bytes) -> bytes | None: """Send DESFire command via I-block, return response (status + data).""" pcb = 0x02 resp = run(tag.handle_frame(RFFrame.from_bytes(bytes([pcb]) + cmd))) if resp is None: return None return resp.data[1:] # strip PCB # --------------------------------------------------------------------------- # DesfireReader # --------------------------------------------------------------------------- class TestDesfireReader: def test_get_version(self): medium = SoftwareMedium() tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06") run(medium.attach(tag)) reader = DesfireReader(medium) result = run(reader.get_version(uid=b"\x04\x01\x02\x03\x04\x05\x06")) assert result is not None assert "hw_vendor" in result def test_create_and_select_app(self): medium = SoftwareMedium() tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06") run(medium.attach(tag)) reader = DesfireReader(medium) uid = b"\x04\x01\x02\x03\x04\x05\x06" result = run(reader.create_application(uid, aid=b"\x01\x02\x03", num_keys=1)) assert result["success"] result = run(reader.select_application(uid, aid=b"\x01\x02\x03")) assert result["success"]