"""Bit-field register infrastructure + the T5577 config register. Hardware-free.""" import pytest from pm3py.transponders.bitfield import BitField, Register from pm3py.sim import ( T5577Config, T5577Tag, Ntag21xConfig, NTAG213, MF0UL21, MifareUltralightC, MifareAccessConditions, MifareClassicTag, EM4100Code, EM4100Tag, NtagI2CConfig, NtagI2CSession, NtagI2CPtI2C, NT3H2111, NT3H2211, Ntag5StatusReg, Ntag5ConfigReg, Ntag5LinkTag, NxpKeyPrivileges, IcodeDnaTag, IcodeFeatureFlags, ) class Demo(Register): _width_bits = 16 _label = "demo" top = BitField(15, 12, doc="top nibble") flag = BitField(3, doc="a flag") mode = BitField(1, 0, values={0: "a", 1: "b", 2: "c"}, doc="a labelled mode") class TestBitFieldCore: def test_read_fields(self): d = Demo(0xF008) # top=0xF, flag(bit3)=1, mode=0 assert d.top == 0xF assert d.flag is True assert d.mode == "a" def test_single_bit_is_bool(self): assert Demo(0).flag is False assert Demo(0x8).flag is True def test_write_by_field_is_isolated(self): d = Demo(0) d.top = 0xA assert d.value == 0xA000 d.flag = True assert d.value == 0xA008 d.top = 0x5 # must not disturb the flag bit assert d.value == 0x5008 def test_values_map_both_directions(self): d = Demo(0) d.mode = "c" assert d.mode == "c" and d.value == 0x0002 d.mode = 1 # raw code still accepted assert d.mode == "b" def test_overflow_rejected(self): with pytest.raises(ValueError): Demo(0).top = 0x1F # 5 bits into a 4-bit field def test_int_and_index(self): d = Demo(0x1234) assert int(d) == 0x1234 assert hex(d) == "0x1234" def test_fields_ordered_high_to_low(self): assert [n for n, _ in Demo.fields()] == ["top", "flag", "mode"] def test_descriptor_doc_visible(self): # the completion tooltip / `field?` help comes from the descriptor docstring assert Demo.top.__doc__ == "top nibble" # The three anchor constants below are lifted from the Proxmark3 config-block table # (client/src/cmdlft55xx.h) with their documented meanings — they pin the bit layout. class TestT5577Decode: def test_em4100(self): cfg = T5577Config(0x00148040) assert cfg.modulation == "ASK" assert cfg.data_bit_rate == 64 assert cfg.max_block == 2 assert cfg.x_mode is False assert cfg.st is False assert cfg.master_key == "0 (normal)" def test_default_ask_manchester(self): cfg = T5577Config(0x000880E8) # ASK, RF/32, STT, 7 data blocks assert cfg.modulation == "ASK" assert cfg.data_bit_rate == 32 assert cfg.max_block == 7 assert cfg.st is True def test_pac_is_nrz(self): cfg = T5577Config(0x00080080) # NRZ, RF/32, 4 data blocks assert cfg.modulation == "direct" assert cfg.max_block == 4 def test_hid_fsk(self): cfg = T5577Config(0x00107060) # FSK2a, RF/50, 3 blocks assert cfg.modulation == "FSK2a" assert cfg.data_bit_rate == 50 assert cfg.max_block == 3 def test_xmode_data_rate(self): cfg = T5577Config(0x903F0082) # fdx-b, x-mode assert cfg.x_mode is True assert cfg.modulation == "biphase" assert cfg.data_bit_rate == 32 # 2*code+2 over the 6-bit field class TestT5577Build: def test_preset_roundtrip(self): assert int(T5577Config.preset("em4100")) == 0x00148040 def test_unknown_preset(self): with pytest.raises(ValueError): T5577Config.preset("nope") def test_edit_by_field(self): cfg = T5577Config.preset("em4100") cfg.max_block = 3 cfg.st = True # max_block bits 7:5 -> 3<<5 = 0x60; st bit3 -> 0x08; base had maxblk 2 (0x40) assert cfg.max_block == 3 and cfg.st is True assert int(cfg) == (0x00148040 & ~(0x07 << 5)) | (3 << 5) | (1 << 3) def test_set_data_rate_standard(self): cfg = T5577Config(0x00148040) cfg.data_bit_rate = 32 assert cfg.data_bit_rate == 32 with pytest.raises(ValueError): cfg.data_bit_rate = 33 # not a legal standard divider def test_set_modulation_by_name(self): cfg = T5577Config(0) cfg.modulation = "FSK2" assert cfg.modulation == "FSK2" assert cfg.modulation != "ASK" def test_decode_mapping_and_repr(self): cfg = T5577Config(0x00148040) d = cfg.decode() assert d["modulation"] == "ASK" assert d["data_bit_rate"] == "RF/64" assert d["max_block"] == "2" text = repr(cfg) # not a tty in tests -> plain text assert "T5577 config (block0) = 0x00148040" in text assert "modulation" in text and "ASK" in text assert "data_bit_rate" in text and "RF/64" in text class TestT5577TagBinding: def test_config_reads_block0(self): tag = T5577Tag() tag.blocks[0] = 0x00148040 assert tag.config.modulation == "ASK" assert tag.config.max_block == 2 def test_assign_config_programs_block0(self): tag = T5577Tag() cfg = T5577Config.preset("hid") tag.config = cfg assert tag.blocks[0] == 0x00107060 # accepts a raw int too tag.config = 0x00148040 assert tag.config.modulation == "ASK" class TestNtag21xConfig: def test_fields_span_both_pages(self): cfg = Ntag21xConfig(0) cfg.auth0 = 0x04 # CFG0 byte 3 cfg.prot = True # CFG1 byte 0 bit 7 (ACCESS.PROT) # CFG0 = 00 00 00 04, CFG1 = 80 00 00 00 assert bytes(cfg) == b"\x00\x00\x00\x04\x80\x00\x00\x00" assert cfg.auth0 == 0x04 and cfg.prot is True def test_mirror_and_counter_fields(self): cfg = Ntag21xConfig(0) cfg.mirror_conf = "UID+counter" cfg.mirror_page = 0x10 cfg.nfc_cnt_en = True cfg.authlim = 5 assert cfg.mirror_conf == "UID+counter" assert cfg.mirror_page == 0x10 assert cfg.nfc_cnt_en is True and cfg.authlim == 5 # re-decoding the raw word must reproduce everything assert Ntag21xConfig(int(cfg)).decode() == cfg.decode() def test_tag_binding_roundtrip(self): tag = NTAG213() # fresh: auth0=0xFF, prot off assert tag.config.auth0 == 0xFF assert tag.config.prot is False cfg = tag.config cfg.auth0 = 0x10 cfg.prot = True tag.config = cfg # program both config pages assert tag.config.auth0 == 0x10 and tag.config.prot is True class TestUltralightConfig: def test_ev1_uses_shared_fields(self): tag = MF0UL21() cfg = tag.config cfg.auth0 = 0x08 cfg.cfglck = True tag.config = cfg assert tag.config.auth0 == 0x08 and tag.config.cfglck is True assert "NTAG21x" not in repr(cfg) # EV1 has no mirror/counter fields def test_ulc_auth_config(self): tag = MifareUltralightC(auth0=0x30, auth1_read_protect=True) cfg = tag.config assert cfg.auth0 == 0x30 assert cfg.auth1_prot == "read+write" # AUTH1 bit0 == 0 cfg.auth1_prot = "write-only" cfg.auth0 = 0x2A tag.config = cfg assert tag.config.auth0 == 0x2A assert tag.config.auth1_prot == "write-only" class TestMifareAccessConditions: def test_factory_default_trailer(self): # FF 07 80 access bytes + GPB 0x69 = the transport/default trailer ac = MifareAccessConditions(0xFF078069) assert ac.valid is True assert ac.block0 == 0 and ac.block1 == 0 and ac.block2 == 0 assert ac.trailer == 1 assert ac.gpb == 0x69 def test_build_matches_default(self): ac = MifareAccessConditions.build(0, 0, 0, trailer=1, gpb=0x69) assert int(ac) == 0xFF078069 assert bytes(ac) == b"\xFF\x07\x80\x69" # trailer bytes 6..9 def test_build_is_always_valid(self): for t in range(8): for d in range(8): assert MifareAccessConditions.build(d, d, d, trailer=t).valid def test_all_locked(self): ac = MifareAccessConditions.build(7, 7, 7, trailer=7) assert ac.trailer == 7 and ac.block0 == 7 assert "no access" in ac.describe(0) assert "locked" in ac.describe(3) def test_inconsistent_copy_flagged(self): ac = MifareAccessConditions(0x00000000) # C == ~C == 0, impossible assert ac.valid is False assert "INVALID" in repr(ac) def test_bad_condition_rejected(self): with pytest.raises(ValueError): MifareAccessConditions.build(trailer=8) def test_describe_and_decode(self): ac = MifareAccessConditions(0xFF078069) assert "transport" in ac.describe(0) assert "transport/default" in ac.describe(3) d = ac.decode() assert d["block0"].startswith("0") assert d["trailer"].startswith("1") def test_tag_access_reads_trailer(self): tag = MifareClassicTag(uid=b"\x01\x02\x03\x04") # write a full trailer for sector 1: KeyA(6) + FF0780 + GPB69 + KeyB(6) trailer_blk = tag._sector_trailer_block(1) trailer = b"\xA0\xA1\xA2\xA3\xA4\xA5" + b"\xFF\x07\x80\x69" + b"\xB0\xB1\xB2\xB3\xB4\xB5" tag.write_block_raw(trailer_blk, trailer) ac = tag.access(1) assert ac.trailer == 1 and ac.gpb == 0x69 and ac.valid class TestEM4100Code: def test_build_roundtrip(self): code = EM4100Code.build(0x0102030405) assert code.tag_id == 0x0102030405 assert code.customer_id == 0x01 assert code.card_number == 0x02030405 assert code.valid def test_build_is_always_valid(self): for tid in (0x0, 0xFFFFFFFFFF, 0x1234567890, 0xDEADBEEF00, 0xA5A5A5A5A5): assert EM4100Code.build(tid).valid def test_matches_model_encoding(self): # the register must agree bit-for-bit with the tag's own encoder tag = EM4100Tag(0xCAFEBABE99) assert tag.code.valid assert tag.code.tag_id == 0xCAFEBABE99 assert int(tag.code) == int(EM4100Code.build(0xCAFEBABE99)) def test_flipped_data_bit_breaks_row_parity(self): code = EM4100Code.build(0x0102030405) broken = EM4100Code(int(code) ^ (1 << 40)) # flip a data bit in the rows region assert broken.valid is False assert broken._bad_rows() # at least one row parity fails def test_bad_header_and_stop_detected(self): good = EM4100Code.build(0) assert good.valid assert EM4100Code(int(good) | 1).valid is False # stop bit set assert EM4100Code(int(good) & ~(0x1FF << 55)).valid is False # header zeroed def test_repr_shows_id_and_validity(self): text = repr(EM4100Code.build(0x0102030405)) assert "EM4100 64-bit code" in text assert "0102030405" in text assert "valid" in text and "yes" in text class TestNtagI2CRegisters: def test_fresh_config_defaults(self): # NT3H2111 ships E8=01 00 F8 48, E9=08 01 00 00 (datasheet defaults) cfg = NT3H2111().config assert cfg.transfer_dir == "NFC->I2C" # NC_REG bit0 = 1 assert cfg.pthru_on is False assert cfg.sram_mirror_on is False assert cfg.sram_mirror_block == 0xF8 assert cfg.watchdog == 0x0848 assert cfg.i2c_clock_str is True assert cfg.reg_lock_i2c is False and cfg.reg_lock_nfc is False def test_nc_reg_fields_roundtrip(self): tag = NT3H2111() cfg = tag.config cfg.pthru_on = True cfg.sram_mirror_on = True cfg.fd_on = "tag selected" # 0b10 cfg.fd_off = "field off / last NDEF read" # 0b10 cfg.last_ndef_block = 0x2A tag.config = cfg # program E8/E9 back = tag.config assert back.pthru_on is True and back.sram_mirror_on is True assert back.fd_on == "tag selected" assert back.fd_off == "field off / last NDEF read" assert back.last_ndef_block == 0x2A def test_reg_lock(self): cfg = NtagI2CConfig(0) cfg.reg_lock_nfc = True # REG_LOCK is byte 6 (bits 15:8); bit0 = NFC lock -> byte6 == 0x01 assert bytes(cfg)[6] == 0x01 assert cfg.reg_lock_nfc is True and cfg.reg_lock_i2c is False def test_session_ns_reg_decode(self): # NS_REG is byte 6 of the EC/ED block; set NDEF_DATA_READ + RF_FIELD_PRESENT + busy sess = NtagI2CSession(0) assert sess.rf_field_present is False raw = int(NtagI2CSession(0)) # byte6 bit7 (ndef_data_read)=bit15, bit0 (rf_field_present)=bit8, bit1 (busy)=bit9 sess = NtagI2CSession((1 << 15) | (1 << 8) | (1 << 9)) assert sess.ndef_data_read is True assert sess.rf_field_present is True assert sess.eeprom_wr_busy is True assert sess.rf_locked is False def test_session_binding(self): tag = NT3H2111() sess = tag.session # session copy shares NC_REG defaults (transfer_dir=1) assert sess.transfer_dir == "NFC->I2C" assert sess.rf_field_present is False # NS_REG starts clear def test_pt_i2c(self): pt = NtagI2CPtI2C(0) pt.sram_prot = True pt.i2c_prot = "protected area read-only" # 0b01, unambiguous assert pt.sram_prot is True assert pt.i2c_prot == "protected area read-only" assert int(pt) == (1 << 2) | (1 << 0) # datasheet 1Xb == "no access to protected area" for both codes 2 and 3 assert NtagI2CPtI2C(2).i2c_prot == "protected area no access" assert NtagI2CPtI2C(3).i2c_prot == "protected area no access" # tag binding tag = NT3H2211() tag.pt_i2c = pt assert tag.pt_i2c.i2c_prot == "protected area read-only" def test_repr_is_decode_table(self): text = repr(NT3H2111().config) assert "NTAG I2C config registers" in text assert "transfer_dir" in text and "NFC->I2C" in text assert "sram_mirror_block" in text class TestNtag5Registers: def test_config_reg_field_positions(self): cfg = Ntag5ConfigReg(0) cfg.arbiter_mode = "pass-through" # 0b10 at byte0 bits 3:2 cfg.sram_enable = True # byte0 bit1 cfg.disable_nfc = True # byte0 bit5 cfg.config_pt_transfer_dir = "NFC->I2C" # byte0 bit0 # byte0 = 0b0010_1011 = 0x2B (disable_nfc bit5 + arb=10 at bit3 + sram bit1 + ptdir bit0) assert bytes(cfg)[0] == 0x2B assert cfg.arbiter_mode == "pass-through" assert cfg.disable_nfc is True def test_status_reg_decode(self): # byte0: nfc_field_ok(0)+vcc_supply_ok(1)+eeprom_wr_busy(7); byte1: nfc_if_locked(0) st = Ntag5StatusReg(int.from_bytes(bytes([0b1000_0011, 0x01, 0, 0]), "big")) assert st.nfc_field_ok is True assert st.vcc_supply_ok is True assert st.eeprom_wr_busy is True assert st.sram_data_ready is False assert st.nfc_if_locked is True def test_view_agrees_with_existing_accessors(self): tag = Ntag5LinkTag() tag.arbiter_mode = 2 # existing per-field setter tag.disable_nfc = True tag.sram_enable = True # the register view must decode the very same bytes assert tag.config_reg.arbiter_mode == "pass-through" assert tag.config_reg.disable_nfc is True assert tag.config_reg.sram_enable is True def test_assign_config_reg_updates_accessors_and_siblings(self): tag = Ntag5LinkTag() cfg = Ntag5ConfigReg(0) cfg.arbiter_mode = "mirror" # 1 cfg.sram_enable = True tag.config_reg = cfg # existing property accessors (read straight off the bytes) must reflect it assert tag.arbiter_mode == 1 assert tag.sram_enable is True # and the sibling caches the individual setters maintain stay in sync if hasattr(tag, "_arbiter_mode"): assert tag._arbiter_mode == 1 if hasattr(tag, "_sram_enabled"): assert tag._sram_enabled is True def test_status_reg_binding_roundtrip(self): tag = Ntag5LinkTag() st = tag.status_reg st.i2c_if_locked = True 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 class TestIcodeFeatureFlags: def test_bits_match_module_constants(self): import pm3py.transponders.hf.iso15693.nxp.icode3 as ic3 # every declared field must line up with the _FEATURE_* constant of the same name for name, bf in IcodeFeatureFlags.fields(): const = getattr(ic3, f"_FEATURE_{name.upper()}") assert (1 << bf.lo) == const, name def test_decode(self): # um_pp(0) + counter(1) + originality_sig(8) + high_data_rates(15) w = (1 << 0) | (1 << 1) | (1 << 8) | (1 << 15) f = IcodeFeatureFlags(w) assert f.um_pp is True and f.counter is True assert f.originality_sig is True and f.high_data_rates is True assert f.privacy is False assert int(f) == w def test_repr(self): text = repr(IcodeFeatureFlags(0x0001)) assert "ICODE feature flags" in text assert "um_pp" in text