Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/catalog/gap-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ audit (`docs/refactor/v0.8-team-audit.md`).
| ISO15693-3 writer | firmware §4.2 #4 | ❌ | — | — | **NEW** small gap |
| EMV parse (visa/mc) | firmware §4.2 #3 | ✅ `nfc_emv_decode` (+ `nfc_emv_encode`) — BER-TLV walker + ~80-tag dictionary; `nfc_emv_track2_decode` v0.414 cracks tag 57 (PAN/expiry/service code, Luhn-gated); `nfc_emv_dol_decode` v0.415 walks PDOL/CDOL/DDOL/TDOL (tag,length) lists; `nfc_emv_afl_decode` v0.416 expands tag 94 (SFI/record ranges → READ RECORD list); `nfc_emv_cvm_decode` v0.426 cracks tag 8E (X/Y amounts + CVM method/condition rules) | — | — | shipped — BER-TLV + Track-2 + DOL + AFL + CVM-List field decode. Cryptogram/online-auth flow deliberately out of scope (needs issuer keys). |
| Wiegand D0/D1 capture + replay | apps top-20 #6 + attacks #6 | ❌ | — | — | **§2b** ⟶ `gpio_wiegand_capture/replay` |
| HID Prox / EM4xxx PACS decode | apps top-20 #15 | ⚠️ `rfid_raw_analyze` | — | ✅ pm3 | **NEW** ⟶ `rfid_pacs_decode` |
| HID Prox / EM4xxx PACS decode | apps top-20 #15 | ✅ `rfid_pacs_decode` (+ inverse `rfid_pacs_encode`: H10301/H10306/H10304/H10302-37bit, round-trip-verified) | — | ✅ pm3 | shipped — Wiegand PACS field decode (see §3 #19) |
| LF EM4100 / T5577 read+write | baseline | ✅ `rfid_*`, `loader_t5577_multiwriter`; offline `em4100_decode` (ID forms) + `em4100_encode` (64-bit frame) + `em4100_frame_decode` v0.417 (parity-validating frame→ID inverse) | — | — | — |
| FDX-B / DCF77 / niche LF synth | apps NFC | ⚠️ `rfid_build` covers EM4100 only; **FDX-B offline *decode* shipped v0.464.0 (`fdxb_decode`)**, DCF77 telegram-synth v0.375 | — | — | Low-priority gaps (LF *synth*/build side remains EM4100-only) |
| UHF EPC Gen2 (M6E-Nano) | apps `uhf_rfid` | ⚠️ | — | — | Adjacent-HW gap for *reading*; **offline EPC *decode* shipped v0.468.0 (`epc_decode`, SGTIN-96)** |
Expand Down
116 changes: 116 additions & 0 deletions internal/tools/decoder_fuzz_safety_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,122 @@ func TestTextDecoders_MalformedInputNeverPanics(t *testing.T) {
t.Logf("panic-safety swept %d text decoders", tested)
}

// isMultiParamHostDecoder reports whether a Spec is a pure offline host
// tool with two or more required params — the family the single-param hex
// and text guards above deliberately skip (each requires exactly one
// param, so the multi-param encoders, KDFs and hashcat-line formatters
// fall through both). Encoders (rfid_pacs_encode, ioprox_encode,
// dcf77_synth), KDFs (wpa_pmk_derive, hmac_compute, postgres_password)
// and the wifi_*_hc22000 formatters all live here: each parses structured
// hex / string / integer fields an operator may paste truncated or
// malformed, and none may panic. GroupHostTools + risk.Low keeps the
// sweep to host-only tools that ignore Deps, so a nil Deps is safe (the
// same exclusion the single-param guards rely on).
func isMultiParamHostDecoder(s Spec) bool {
return s.Handler != nil &&
s.Group == GroupHostTools &&
s.Risk == risk.Low &&
len(s.Required) >= 2
}

// malformedByType returns degenerate values appropriate to a required
// param's JSON-schema type. Numeric params get boundary and overflow
// values (a count field trusted before allocation/slicing); everything
// else is treated as a string and fed the truncated-hex / empty /
// stray-separator battery that has historically tripped length-field
// parsers.
func malformedByType(jsonType string) []any {
switch jsonType {
case "integer", "number":
return []any{0, 1, -1, -2147483648, 2147483647, 4294967296, 9999999999, 64, 256}
default:
return []any{"", " ", "0", "zz", "::::", "ffffffffffffffff", "\x00\xff", "...", "-1",
strings.Repeat("f", 9000)}
}
}

// TestMultiParamDecoders_MalformedInputNeverPanics extends the panic-safety
// net to every pure offline host tool with two or more required params —
// the gap the single-param hex/text guards leave open. For each such tool
// it sweeps one param at a time across its type's degenerate battery while
// the remaining required params hold a fixed degenerate anchor, plus an
// all-params-degenerate pass. A new multi-field encoder/decoder added
// without bounds-checking a short hex field or an out-of-range count would
// pass its own happy-path tests but trip here.
//
// Errors are expected and fine — the contract is "return an error or a
// benign Result, never panic".
func TestMultiParamDecoders_MalformedInputNeverPanics(t *testing.T) {
tested := 0
for _, s := range All() {
if !isMultiParamHostDecoder(s) {
continue
}
var parsed struct {
Properties map[string]struct {
Type string `json:"type"`
} `json:"properties"`
}
if err := json.Unmarshal(s.Schema, &parsed); err != nil {
continue
}
tested++

// A fixed type-appropriate anchor for the params we are not
// currently varying, so the handler clears each param's early
// validation and reaches the deeper field-parsing code with a
// single hostile value at a time.
anchor := func(p string) any {
if parsed.Properties[p].Type == "integer" || parsed.Properties[p].Type == "number" {
return 1
}
return "ffffffffffffffff"
}

call := func(args map[string]any) {
defer func() {
if r := recover(); r != nil {
t.Errorf("%s panicked on args=%v: %v", s.Name, args, r)
}
}()
_, _ = s.Handler(context.Background(), nil, args)
}

// Sweep each required param across its battery, anchoring the rest.
for _, target := range s.Required {
for _, bad := range malformedByType(parsed.Properties[target].Type) {
args := map[string]any{}
for _, p := range s.Required {
args[p] = anchor(p)
}
args[target] = bad
call(args)
}
}

// All params degenerate at once: empty strings / zero ints, then
// max-ish ints / long hex.
for _, mode := range []int{0, 1} {
args := map[string]any{}
for _, p := range s.Required {
b := malformedByType(parsed.Properties[p].Type)
if mode == 0 {
args[p] = b[0] // "" or 0
} else {
args[p] = b[len(b)-1] // long-hex or 9999999999
}
}
call(args)
}
}
// Sanity floor: the multi-param host family is ~15 tools today. If
// this collapses the filter regressed and the guard is inert.
if tested < 10 {
t.Errorf("only %d multi-param host decoders exercised; expected 10+ — filter may have regressed", tested)
}
t.Logf("panic-safety swept %d multi-param host decoders", tested)
}

// hexDecoderHandlers caches the pure hex-decoder handlers once so the
// fuzz body doesn't re-scan the registry on every input.
func hexDecoderHandlers() []Spec {
Expand Down
Loading