Skip to content

Commit b2b3e6c

Browse files
committed
Add memory probe fallback for NTAG detection when GET_VERSION fails
Some NTAG216 cards intermittently fail GET_VERSION (status 6300) due to communication timing issues. When both GET_VERSION and CC detection fail, the agent now probes actual card memory capacity by reading high pages: - Page 135 readable → NTAG216 (888 bytes) - Page 45 readable → NTAG215 (504 bytes) - Page 16 readable → NTAG213 (180 bytes) - All fail → MIFARE Ultralight (64 bytes) This approach is future-proof as it tests actual card capability rather than relying on metadata that can fail intermittently. Fixes customer-reported issue where NTAG216 was misidentified as MIFARE Ultralight with 64 bytes.
1 parent 5eb4d67 commit b2b3e6c

File tree

3 files changed

+186
-14
lines changed

3 files changed

+186
-14
lines changed

internal/core/card.go

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,50 @@ func detectCardType(card *scard.Card, cardInfo *Card) {
416416
}
417417
}
418418

419-
// Method 3: Check ATR patterns for NTAG, MIFARE, and ISO 15693
419+
// Method 3: Memory probe - determine card capacity by reading high page numbers
420+
// This is used when GET_VERSION and CC detection both failed but ATR suggests ISO 14443-3A Type 2.
421+
// Unlike ICode SLIX (ISO 15693) which has type indicator bits in the UID, NTAG21x cards
422+
// follow ISO 14443-3A which has NO type identifiers in the UID. We must probe actual memory.
423+
// Page boundaries: Ultralight=16, NTAG213=45, NTAG215=135, NTAG216=231
424+
if contains(atr, "03060300") && !ccDetectionFoundNDEF && !getVersionSucceeded {
425+
logging.Debug(logging.CatCard, "Starting memory probe detection", nil)
426+
427+
// Try page 135 (only NTAG216 has this - it has 231 pages)
428+
if _, err := readNTAGPage(card, 135); err == nil {
429+
logging.Debug(logging.CatCard, "Memory probe: page 135 readable, detected NTAG216", nil)
430+
cardInfo.Type = "NTAG216"
431+
cardInfo.Size = 888
432+
cardInfo.Writable = true
433+
return
434+
}
435+
436+
// Try page 45 (NTAG215 has 135 pages, NTAG213 has 45, Ultralight has 16)
437+
if _, err := readNTAGPage(card, 45); err == nil {
438+
logging.Debug(logging.CatCard, "Memory probe: page 45 readable, detected NTAG215", nil)
439+
cardInfo.Type = "NTAG215"
440+
cardInfo.Size = 504
441+
cardInfo.Writable = true
442+
return
443+
}
444+
445+
// Try page 16 (NTAG213 has 45 pages, Ultralight only has 16)
446+
if _, err := readNTAGPage(card, 16); err == nil {
447+
logging.Debug(logging.CatCard, "Memory probe: page 16 readable, detected NTAG213", nil)
448+
cardInfo.Type = "NTAG213"
449+
cardInfo.Size = 180
450+
cardInfo.Writable = true
451+
return
452+
}
453+
454+
// All probes failed - this is likely actual MIFARE Ultralight
455+
logging.Debug(logging.CatCard, "Memory probe: all high pages failed, assuming MIFARE Ultralight", nil)
456+
cardInfo.Type = "MIFARE Ultralight"
457+
cardInfo.Size = 64
458+
cardInfo.Writable = true
459+
return
460+
}
461+
462+
// Method 4: Check ATR patterns for NTAG, MIFARE, and ISO 15693
420463
// Note: atr is already set at the start of detectCardType for protocol detection
421464
if len(atr) >= 30 && (atr[0:4] == "3b8f" || atr[0:4] == "3b8b") {
422465
// Check for ISO 15693 cards (ICode SLI, ICode Slix, ICode Slix 2)
@@ -481,20 +524,11 @@ func detectCardType(card *scard.Card, cardInfo *Card) {
481524
cardInfo.Writable = true
482525
cardInfo.Size = 1024
483526
return
484-
} else if atr[28:30] == "03" {
485-
// ISO 14443-3A Type 2 tag (could be NTAG or MIFARE Ultralight)
486-
// If we found valid NDEF CC earlier, CC detection should have identified it.
487-
// If we reach here with valid NDEF, it means CC size didn't match known types.
488-
// If CC detection failed (no valid NDEF), this is likely plain MIFARE Ultralight
489-
// without NDEF formatting, or with non-standard CC.
490-
if !ccDetectionFoundNDEF && !getVersionSucceeded {
491-
cardInfo.Type = "MIFARE Ultralight"
492-
cardInfo.Size = 64 // Plain Ultralight has 64 bytes (48 user bytes)
493-
cardInfo.Writable = true
494-
return
495-
}
496-
// Otherwise report unknown - we have NDEF but unknown CC size
497527
}
528+
// Note: For byte 14 == "03" (ISO 14443-3A Type 2 tags like NTAG/Ultralight),
529+
// detection is handled by Method 3 (memory probe) above when GET_VERSION and CC both failed.
530+
// If we reach here, either GET_VERSION or CC succeeded but didn't identify the type,
531+
// which means the CC size didn't match known types - fall through to unknown.
498532
}
499533

500534
// Fallback: if ATR starts with 3b8f/3b8b but doesn't match above patterns

internal/core/card_detection_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,90 @@ func TestICodeSLIXDetection(t *testing.T) {
504504
}
505505
}
506506

507+
// TestMemoryProbeDetection tests that cards are correctly identified via memory probe
508+
// when GET_VERSION and CC detection both fail. This is critical for NTAG cards
509+
// where GET_VERSION fails intermittently due to communication issues.
510+
func TestMemoryProbeDetection(t *testing.T) {
511+
tests := []struct {
512+
name string
513+
mockCardType string
514+
expectedType string
515+
expectedSize int
516+
}{
517+
{
518+
name: "NTAG216 detected via memory probe (page 135 readable)",
519+
mockCardType: "NTAG216-MemoryProbe",
520+
expectedType: "NTAG216",
521+
expectedSize: 888,
522+
},
523+
{
524+
name: "NTAG215 detected via memory probe (page 45 readable, page 135 fails)",
525+
mockCardType: "NTAG215-MemoryProbe",
526+
expectedType: "NTAG215",
527+
expectedSize: 504,
528+
},
529+
}
530+
531+
for _, tt := range tests {
532+
t.Run(tt.name, func(t *testing.T) {
533+
card := NewMockCard(tt.mockCardType)
534+
535+
// Verify GET_VERSION fails (returns 6300)
536+
getVersionCmd := []byte{0xFF, 0x00, 0x00, 0x00, 0x02, 0x60, 0x00}
537+
resp, err := card.Transmit(getVersionCmd)
538+
if err != nil {
539+
t.Fatalf("Transmit failed: %v", err)
540+
}
541+
if len(resp) >= 2 && resp[len(resp)-2] == 0x90 && resp[len(resp)-1] == 0x00 {
542+
t.Error("GET_VERSION should fail for memory probe test cards")
543+
}
544+
545+
// Verify CC has no valid NDEF magic byte
546+
ccReadCmd := []byte{0xFF, 0xB0, 0x00, 0x03, 0x10}
547+
resp, err = card.Transmit(ccReadCmd)
548+
if err != nil {
549+
t.Fatalf("CC read failed: %v", err)
550+
}
551+
if len(resp) >= 1 && resp[0] == 0xE1 {
552+
t.Error("CC should not have valid NDEF magic byte for memory probe test")
553+
}
554+
555+
// Verify the expected memory probe page succeeds
556+
if tt.expectedType == "NTAG216" {
557+
// Page 135 should be readable
558+
readPage135 := []byte{0xFF, 0xB0, 0x00, 0x87, 0x04}
559+
resp, err = card.Transmit(readPage135)
560+
if err != nil {
561+
t.Fatalf("Page 135 read failed: %v", err)
562+
}
563+
if len(resp) < 2 || resp[len(resp)-2] != 0x90 || resp[len(resp)-1] != 0x00 {
564+
t.Error("Page 135 should be readable for NTAG216")
565+
}
566+
} else if tt.expectedType == "NTAG215" {
567+
// Page 135 should fail (NTAG215 only has 135 pages, 0-134)
568+
readPage135 := []byte{0xFF, 0xB0, 0x00, 0x87, 0x04}
569+
resp, err = card.Transmit(readPage135)
570+
if err != nil {
571+
t.Fatalf("Page 135 read command failed: %v", err)
572+
}
573+
if len(resp) >= 2 && resp[len(resp)-2] == 0x90 && resp[len(resp)-1] == 0x00 {
574+
t.Error("Page 135 should NOT be readable for NTAG215")
575+
}
576+
577+
// Page 45 should be readable
578+
readPage45 := []byte{0xFF, 0xB0, 0x00, 0x2D, 0x04}
579+
resp, err = card.Transmit(readPage45)
580+
if err != nil {
581+
t.Fatalf("Page 45 read failed: %v", err)
582+
}
583+
if len(resp) < 2 || resp[len(resp)-2] != 0x90 || resp[len(resp)-1] != 0x00 {
584+
t.Error("Page 45 should be readable for NTAG215")
585+
}
586+
}
587+
})
588+
}
589+
}
590+
507591
// hexEncodeString is a helper to encode bytes to hex string
508592
func hexEncodeString(b []byte) string {
509593
const hexChars = "0123456789abcdef"

internal/core/mock_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,19 @@ func NewMockCard(cardType string) *MockSmartCard {
120120
card.uid, _ = hex.DecodeString("ff0f39c8d60000")
121121
card.cardType = "MIFARE Ultralight"
122122
card.setupMIFAREUltralightResponses()
123+
case "NTAG216-MemoryProbe":
124+
// Simulates NTAG216 where GET_VERSION fails but memory probe succeeds
125+
// This tests the fallback detection path
126+
card.atr, _ = hex.DecodeString("3b8f8001804f0ca0000003060300030000000068")
127+
card.uid, _ = hex.DecodeString("04eeed91ca2a81") // Real NTAG216 UID from customer
128+
card.cardType = "NTAG216"
129+
card.setupNTAG216MemoryProbeResponses()
130+
case "NTAG215-MemoryProbe":
131+
// Simulates NTAG215 where GET_VERSION fails but memory probe succeeds
132+
card.atr, _ = hex.DecodeString("3b8f8001804f0ca0000003060300030000000068")
133+
card.uid, _ = hex.DecodeString("045d4737c62a81") // Real NTAG215 UID
134+
card.cardType = "NTAG215"
135+
card.setupNTAG215MemoryProbeResponses()
123136
default:
124137
card.atr, _ = hex.DecodeString("3b8f8001804f0ca0000003060300030000000068")
125138
card.uid, _ = hex.DecodeString("04000000000000")
@@ -222,6 +235,47 @@ func (m *MockSmartCard) setupMIFAREUltralightResponses() {
222235
m.responses["ffb0000110"] = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x00}
223236
}
224237

238+
// setupNTAG216MemoryProbeResponses simulates an NTAG216 where GET_VERSION fails
239+
// but memory probe succeeds (can read page 135). This tests the fallback detection.
240+
func (m *MockSmartCard) setupNTAG216MemoryProbeResponses() {
241+
uidResponse := append(m.uid, 0x90, 0x00)
242+
m.responses["ffca000000"] = uidResponse
243+
244+
// GET_VERSION fails with 6300 (simulates intermittent communication failure)
245+
m.responses["ff000000026000"] = []byte{0x63, 0x00}
246+
m.responses["ff0000000160"] = []byte{0x63, 0x00}
247+
248+
// CC reading fails - no valid NDEF magic byte (simulates CC read failure)
249+
m.responses["ffb0000310"] = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x00}
250+
m.responses["ffb0000110"] = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x00}
251+
252+
// Memory probe: page 135 succeeds (NTAG216 has 231 pages)
253+
// Read page 135: FF B0 00 87 04
254+
m.responses["ffb0008704"] = []byte{0x00, 0x00, 0x00, 0x00, 0x90, 0x00}
255+
}
256+
257+
// setupNTAG215MemoryProbeResponses simulates an NTAG215 where GET_VERSION fails
258+
// but memory probe succeeds (page 135 fails, page 45 succeeds).
259+
func (m *MockSmartCard) setupNTAG215MemoryProbeResponses() {
260+
uidResponse := append(m.uid, 0x90, 0x00)
261+
m.responses["ffca000000"] = uidResponse
262+
263+
// GET_VERSION fails with 6300
264+
m.responses["ff000000026000"] = []byte{0x63, 0x00}
265+
m.responses["ff0000000160"] = []byte{0x63, 0x00}
266+
267+
// CC reading fails
268+
m.responses["ffb0000310"] = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x00}
269+
m.responses["ffb0000110"] = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x00}
270+
271+
// Memory probe: page 135 fails (NTAG215 only has 135 pages, 0-134)
272+
m.responses["ffb0008704"] = []byte{0x6A, 0x82} // Page out of range
273+
274+
// Memory probe: page 45 succeeds (NTAG215 has pages 0-134)
275+
// Read page 45: FF B0 00 2D 04
276+
m.responses["ffb0002d04"] = []byte{0x00, 0x00, 0x00, 0x00, 0x90, 0x00}
277+
}
278+
225279
// WithNDEFData sets NDEF data on the mock card
226280
func (m *MockSmartCard) WithNDEFData(ndefType, data string) *MockSmartCard {
227281
m.mu.Lock()

0 commit comments

Comments
 (0)