174 lines
7.3 KiB
Python
174 lines
7.3 KiB
Python
"""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"
|