"""Tests for the MIFARE Ultralight family (original, Ultralight C, EV1).""" import asyncio import os import pytest from pm3py.sim.frame import RFFrame from pm3py.transponders.hf.iso14443a.base import _compute_bcc from pm3py.transponders.hf.iso14443a.nxp.ultralight import ( MifareUltralight, MifareUltralightC, MF0UL11, MF0UL21, UL_C_DEFAULT_KEY, ) UID = b"\x04\x01\x02\x03\x04\x05\x06" def run(coro): return asyncio.new_event_loop().run_until_complete(coro) def select(tag): run(tag.handle_frame(RFFrame.from_hex("26"))) uid = tag._uid ct = b"\x88" + uid[0:3] run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct + bytes([_compute_bcc(ct)])))) cl2 = uid[3:7] run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + cl2 + bytes([_compute_bcc(cl2)])))) assert tag.state == "ACTIVE" def activate(cls, **kw): tag = cls(uid=UID, **kw) run(tag.power_on()) select(tag) return tag def _rotl1(b): return b[1:] + b[:1] # --------------------------------------------------------------------------- # Original MIFARE Ultralight (MF0ICU1) # --------------------------------------------------------------------------- class TestUltralight: def test_16_pages(self): assert MifareUltralight(uid=UID)._total_pages == 16 def test_no_get_version(self): tag = activate(MifareUltralight) assert run(tag.handle_frame(RFFrame.from_bytes(b"\x60"))) is None def test_no_fast_read_or_pwd_auth(self): tag = activate(MifareUltralight) assert run(tag.handle_frame(RFFrame.from_bytes(b"\x3A\x00\x03"))) is None assert run(tag.handle_frame(RFFrame.from_bytes(b"\x1B\x00\x00\x00\x00"))) is None def test_read_write(self): tag = activate(MifareUltralight) assert run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x04\xCA\xFE\xBA\xBE"))).data == b"\x0A" assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x04"))).data[0:4] == b"\xCA\xFE\xBA\xBE" def test_otp_is_or_write(self): tag = activate(MifareUltralight) # page 3 = OTP, starts at 0 run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x03\x01\x00\x00\x00"))) run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x03\x02\x00\x00\x00"))) otp = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x03"))).data[0:4] assert otp == b"\x03\x00\x00\x00" # OR of 0x01 and 0x02 # --------------------------------------------------------------------------- # MIFARE Ultralight C (MF0ICU2) — 3DES # --------------------------------------------------------------------------- class TestUltralightC: def test_48_pages_no_get_version(self): assert MifareUltralightC(uid=UID)._total_pages == 48 tag = activate(MifareUltralightC) assert run(tag.handle_frame(RFFrame.from_bytes(b"\x60"))) is None def test_default_key_installed(self): assert MifareUltralightC(uid=UID).key == UL_C_DEFAULT_KEY def _do_auth(self, tag, key=UL_C_DEFAULT_KEY): from Crypto.Cipher import DES3 r = run(tag.handle_frame(RFFrame.from_bytes(b"\x1A\x00"))) assert r.data[0] == 0xAF and len(r.data) == 9 ek_rndb = r.data[1:9] rndb = DES3.new(key, DES3.MODE_CBC, iv=bytes(8)).decrypt(ek_rndb) rnda = os.urandom(8) token = DES3.new(key, DES3.MODE_CBC, iv=ek_rndb).encrypt(rnda + _rotl1(rndb)) r = run(tag.handle_frame(RFFrame.from_bytes(b"\xAF" + token))) if r.data[0] != 0x00: return None got = DES3.new(key, DES3.MODE_CBC, iv=token[8:16]).decrypt(r.data[1:9]) return got == _rotl1(rnda) def test_3des_auth_round_trip(self): tag = activate(MifareUltralightC, auth0=0x04) assert self._do_auth(tag) is True assert tag._auth_state == "AUTHENTICATED" def test_auth_gates_protected_read(self): tag = activate(MifareUltralightC, auth0=0x04, auth1_read_protect=True) assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x04"))).data == b"\x00" assert self._do_auth(tag) is True assert len(run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x04"))).data) == 16 def test_wrong_key_fails(self): tag = activate(MifareUltralightC, auth0=0x04) wrong = bytes(range(16)) assert self._do_auth(tag, key=wrong) is None assert tag._auth_state != "AUTHENTICATED" def test_key_not_readable(self): tag = activate(MifareUltralightC, auth0=0x30) # protection off resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x2C"))) assert resp.data[0:4] == b"\x00\x00\x00\x00" def test_counter_increments(self): tag = activate(MifareUltralightC) run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x29\x03\x00\x00\x00"))) run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x29\x02\x00\x00\x00"))) cnt = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x29"))).data[0:2] assert int.from_bytes(cnt, "little") == 5 # --------------------------------------------------------------------------- # MIFARE Ultralight EV1 (MF0UL11 / MF0UL21) # --------------------------------------------------------------------------- class TestUltralightEV1: @pytest.mark.parametrize("cls,version,pages", [ (MF0UL11, b"\x00\x04\x03\x01\x01\x00\x0B\x03", 20), (MF0UL21, b"\x00\x04\x03\x01\x01\x00\x0E\x03", 41), ]) def test_version_and_pages(self, cls, version, pages): tag = activate(cls) assert run(tag.handle_frame(RFFrame.from_bytes(b"\x60"))).data == version assert cls(uid=UID)._total_pages == pages def test_page3_is_otp_not_cc(self): # EV1 ships with OTP (zeros) at page 3, no CC. tag = activate(MF0UL11) assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x03"))).data[0:4] == b"\x00\x00\x00\x00" def test_ndef_stamps_cc_into_otp(self): # Supplying an NDEF message NDEF-formats the tag: product CC into page 3. tag = activate(MF0UL21, ndef_message=b"\xD1\x01\x04\x54\x02enHi") assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x03"))).data[0:4] == b"\xE1\x10\x10\x00" # NDEF TLV present at page 4 assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x04"))).data[0] == 0x03 def test_three_counters(self): tag = activate(MF0UL21) for n in range(3): assert run(tag.handle_frame(RFFrame.from_bytes(bytes([0x39, n])))).data == b"\x00\x00\x00" run(tag.handle_frame(RFFrame.from_bytes(b"\xA5\x01\x0A\x00\x00\x00"))) # counter 1 += 10 assert int.from_bytes(run(tag.handle_frame(RFFrame.from_bytes(b"\x39\x01"))).data, "little") == 10 assert run(tag.handle_frame(RFFrame.from_bytes(b"\x39\x00"))).data == b"\x00\x00\x00" # unchanged def test_counter_overflow_naks(self): tag = activate(MF0UL11) run(tag.handle_frame(RFFrame.from_bytes(b"\xA5\x00\xFF\xFF\xFF\x00"))) # to 0xFFFFFF assert run(tag.handle_frame(RFFrame.from_bytes(b"\xA5\x00\x01\x00\x00\x00"))).data == b"\x04" def test_check_tearing(self): tag = activate(MF0UL11) assert run(tag.handle_frame(RFFrame.from_bytes(b"\x3E\x00"))).data == b"\xBD" def test_pwd_auth(self): tag = activate(MF0UL21, password=b"\xDE\xAD\xBE\xEF", pack=b"\x12\x34") assert run(tag.handle_frame(RFFrame.from_bytes(b"\x1B\xDE\xAD\xBE\xEF"))).data == b"\x12\x34" assert run(tag.handle_frame(RFFrame.from_bytes(b"\x1B\x00\x00\x00\x00"))).data == b"\x04"