feat(registers): NXP AES key-privilege register (ICODE DNA / NTAG5)

Add NxpKeyPrivileges (Register) for the Table 5 AES key-privilege byte
and bind it on the NxpAesAuth mixin (ICODE DNA + NTAG5 Link/Boost):

- read / write / privacy / destroy / eas_afi / crypto_config /
  area1_read / area1_write, each a documented bit
- NxpKeyPrivileges.build(read=True, write=True, ...) keyword builder
- tag.key_privileges(key_id) / tag.set_key_privileges(key_id, value)

Additive: the existing PRIV_* constants and has_privilege() path are
unchanged and see the same bytes. 5 new tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-14 08:58:24 -07:00
parent f8d01a9120
commit e795b5bb1d
4 changed files with 82 additions and 1 deletions

View File

@@ -60,7 +60,7 @@ pm3py/
bitfield.py # BitField descriptor + Register base — self-describing config/ bitfield.py # BitField descriptor + Register base — self-describing config/
# frame registers (T5577, EM4100, NTAG21x, Ultralight EV1/C, MFC # frame registers (T5577, EM4100, NTAG21x, Ultralight EV1/C, MFC
# access bits, NTAG I2C NC_REG/NS_REG/REG_LOCK/PT_I2C, NTAG5 # 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 # completion + repr decode table. Layouts cross-checked vs the
# firmware submodule + NXP datasheets. # firmware submodule + NXP datasheets.
hf/iso14443a/ # Tag14443A_3/4, MifareClassic, DESFire, NfcType2/4 hf/iso14443a/ # Tag14443A_3/4, MifareClassic, DESFire, NfcType2/4

View File

@@ -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.icode_slix2 import IcodeSlix2Tag
from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag 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.icode_dna import IcodeDnaTag
from pm3py.transponders.hf.iso15693.nxp.auth_aes import NxpKeyPrivileges
from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import ( from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import (
Ntag5PlatformTag, Ntag5StatusReg, Ntag5ConfigReg, Ntag5PlatformTag, Ntag5StatusReg, Ntag5ConfigReg,
) )
@@ -114,6 +115,7 @@ __all__ = [
"Icode3Tag", "IcodeDnaTag", "Icode3Tag", "IcodeDnaTag",
"Ntag5PlatformTag", "Ntag5SwitchTag", "Ntag5LinkTag", "Ntag5BoostTag", "Ntag5PlatformTag", "Ntag5SwitchTag", "Ntag5LinkTag", "Ntag5BoostTag",
"Ntag5StatusReg", "Ntag5ConfigReg", "Ntag5StatusReg", "Ntag5ConfigReg",
"NxpKeyPrivileges",
"DesfireTag", "DesfireReader", "DesfireTag", "DesfireReader",
"xEM", "xNT", "xM1", "FlexDF", "NExT", "MagicMifareClassicTag", "xEM", "xNT", "xM1", "FlexDF", "NExT", "MagicMifareClassicTag",
"MutationFuzzer", "GrammarFuzzer", "MutationFuzzer", "GrammarFuzzer",

View File

@@ -12,6 +12,8 @@ from __future__ import annotations
import os import os
from pm3py.transponders.bitfield import BitField, Register
try: try:
from Crypto.Cipher import AES as _AES from Crypto.Cipher import AES as _AES
_HAS_AES = True _HAS_AES = True
@@ -34,6 +36,39 @@ PRIV_CRYPTO_CONFIG = 0x20
PRIV_AREA1_READ = 0x40 PRIV_AREA1_READ = 0x40
PRIV_AREA1_WRITE = 0x80 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 # NFC Global Crypto Header values
GCH_DEACTIVATED = 0x81 GCH_DEACTIVATED = 0x81
GCH_DEACTIVATED_PRIV_LOCKED = 0x87 GCH_DEACTIVATED_PRIV_LOCKED = 0x87
@@ -83,6 +118,14 @@ class NxpAesAuth:
return False return False
return bool(self._key_privileges[key_id] & privilege) 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: def is_access_enforced(self) -> bool:
"""Check if GCH is activated (>= 0xC1), meaning access conditions are enforced.""" """Check if GCH is activated (>= 0xC1), meaning access conditions are enforced."""
return self._nfc_gch >= GCH_ACTIVATED return self._nfc_gch >= GCH_ACTIVATED

View File

@@ -9,6 +9,7 @@ from pm3py.sim import (
EM4100Code, EM4100Tag, EM4100Code, EM4100Tag,
NtagI2CConfig, NtagI2CSession, NtagI2CPtI2C, NT3H2111, NT3H2211, NtagI2CConfig, NtagI2CSession, NtagI2CPtI2C, NT3H2111, NT3H2211,
Ntag5StatusReg, Ntag5ConfigReg, Ntag5LinkTag, Ntag5StatusReg, Ntag5ConfigReg, Ntag5LinkTag,
NxpKeyPrivileges, IcodeDnaTag,
) )
@@ -431,3 +432,38 @@ class TestNtag5Registers:
tag.status_reg = st tag.status_reg = st
assert tag.i2c_if_locked is True assert tag.i2c_if_locked is True
assert tag.status_reg.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