diff --git a/CLAUDE.md b/CLAUDE.md index a61fd99..207a089 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,7 @@ pm3py/ bitfield.py # BitField descriptor + Register base — self-describing config/ # frame registers (T5577, EM4100, NTAG21x, Ultralight EV1/C, MFC # access bits, NTAG I2C NC_REG/NS_REG/REG_LOCK/PT_I2C, NTAG5 - # STATUS_REG/CONFIG_REG). Fields are documented attrs -> shell + # STATUS_REG/CONFIG_REG, NXP AES key privileges). Documented attrs -> shell # completion + repr decode table. Layouts cross-checked vs the # firmware submodule + NXP datasheets. hf/iso14443a/ # Tag14443A_3/4, MifareClassic, DESFire, NfcType2/4 diff --git a/pm3py/sim/__init__.py b/pm3py/sim/__init__.py index 9f6ba07..c77ec2d 100644 --- a/pm3py/sim/__init__.py +++ b/pm3py/sim/__init__.py @@ -48,6 +48,7 @@ from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag +from pm3py.transponders.hf.iso15693.nxp.auth_aes import NxpKeyPrivileges from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import ( Ntag5PlatformTag, Ntag5StatusReg, Ntag5ConfigReg, ) @@ -114,6 +115,7 @@ __all__ = [ "Icode3Tag", "IcodeDnaTag", "Ntag5PlatformTag", "Ntag5SwitchTag", "Ntag5LinkTag", "Ntag5BoostTag", "Ntag5StatusReg", "Ntag5ConfigReg", + "NxpKeyPrivileges", "DesfireTag", "DesfireReader", "xEM", "xNT", "xM1", "FlexDF", "NExT", "MagicMifareClassicTag", "MutationFuzzer", "GrammarFuzzer", diff --git a/pm3py/transponders/hf/iso15693/nxp/auth_aes.py b/pm3py/transponders/hf/iso15693/nxp/auth_aes.py index 120122e..1727608 100644 --- a/pm3py/transponders/hf/iso15693/nxp/auth_aes.py +++ b/pm3py/transponders/hf/iso15693/nxp/auth_aes.py @@ -12,6 +12,8 @@ from __future__ import annotations import os +from pm3py.transponders.bitfield import BitField, Register + try: from Crypto.Cipher import AES as _AES _HAS_AES = True @@ -34,6 +36,39 @@ PRIV_CRYPTO_CONFIG = 0x20 PRIV_AREA1_READ = 0x40 PRIV_AREA1_WRITE = 0x80 + +class NxpKeyPrivileges(Register): + """NXP ICODE DNA / NTAG 5 AES key-privilege mask (Table 5) — the 8 bits defining what a + successful authentication with a key slot is allowed to do. Build one to configure a slot, + or decode a stored byte:: + + NxpKeyPrivileges.build(read=True, write=True, crypto_config=True) + NxpKeyPrivileges(0x21).crypto_config # -> True + """ + + _width_bits = 8 + _label = "NXP key privileges" + + read = BitField(0, doc="read user memory") + write = BitField(1, doc="write user memory") + privacy = BitField(2, doc="toggle privacy (enable/disable) mode") + destroy = BitField(3, doc="permanently destroy the tag") + eas_afi = BitField(4, doc="set/reset EAS and write the AFI") + crypto_config = BitField(5, doc="change crypto configuration — keys, privileges, GCH") + area1_read = BitField(6, doc="read AREA1 (Link/Boost second memory area)") + area1_write = BitField(7, doc="write AREA1") + + @classmethod + def build(cls, **flags: bool) -> "NxpKeyPrivileges": + """Compose a privilege mask from named flags, e.g. ``build(read=True, write=True)``.""" + valid = {name for name, _ in cls.fields()} + reg = cls(0) + for name, value in flags.items(): + if name not in valid: + raise TypeError(f"unknown privilege {name!r}; valid: {sorted(valid)}") + setattr(reg, name, bool(value)) + return reg + # NFC Global Crypto Header values GCH_DEACTIVATED = 0x81 GCH_DEACTIVATED_PRIV_LOCKED = 0x87 @@ -83,6 +118,14 @@ class NxpAesAuth: return False return bool(self._key_privileges[key_id] & privilege) + def key_privileges(self, key_id: int) -> NxpKeyPrivileges: + """Key slot ``key_id``'s privilege mask as a self-describing :class:`NxpKeyPrivileges`.""" + return NxpKeyPrivileges(self._key_privileges[key_id]) + + def set_key_privileges(self, key_id: int, value: "NxpKeyPrivileges | int") -> None: + """Program key slot ``key_id``'s privilege mask (accepts a register or a raw byte).""" + self._key_privileges[key_id] = int(value) & 0xFF + def is_access_enforced(self) -> bool: """Check if GCH is activated (>= 0xC1), meaning access conditions are enforced.""" return self._nfc_gch >= GCH_ACTIVATED diff --git a/tests/test_bitfield.py b/tests/test_bitfield.py index 5310ace..ad37f7d 100644 --- a/tests/test_bitfield.py +++ b/tests/test_bitfield.py @@ -9,6 +9,7 @@ from pm3py.sim import ( EM4100Code, EM4100Tag, NtagI2CConfig, NtagI2CSession, NtagI2CPtI2C, NT3H2111, NT3H2211, Ntag5StatusReg, Ntag5ConfigReg, Ntag5LinkTag, + NxpKeyPrivileges, IcodeDnaTag, ) @@ -431,3 +432,38 @@ class TestNtag5Registers: tag.status_reg = st assert tag.i2c_if_locked is True assert tag.status_reg.i2c_if_locked is True + + +class TestNxpKeyPrivileges: + def test_build_and_bits_match_constants(self): + from pm3py.transponders.hf.iso15693.nxp.auth_aes import ( + PRIV_READ, PRIV_WRITE, PRIV_CRYPTO_CONFIG, + ) + p = NxpKeyPrivileges.build(read=True, write=True, crypto_config=True) + assert int(p) == PRIV_READ | PRIV_WRITE | PRIV_CRYPTO_CONFIG + assert p.read is True and p.write is True and p.crypto_config is True + assert p.destroy is False + + def test_decode(self): + p = NxpKeyPrivileges(0x21) # bit0 read + bit5 crypto_config + assert p.read is True + assert p.crypto_config is True + assert p.area1_write is False + + def test_build_rejects_unknown(self): + with pytest.raises(TypeError): + NxpKeyPrivileges.build(nonsense=True) + + def test_tag_binding_roundtrip(self): + tag = IcodeDnaTag() + tag.set_key_privileges(1, NxpKeyPrivileges.build(read=True, area1_write=True)) + p = tag.key_privileges(1) + assert p.read is True and p.area1_write is True + assert p.write is False + # the existing has_privilege() path sees the same bits + assert tag._key_privileges[1] == int(p) + + def test_repr(self): + text = repr(NxpKeyPrivileges.build(read=True, destroy=True)) + assert "NXP key privileges" in text + assert "read" in text and "destroy" in text