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:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user