Skip to content

Commit 37c4e90

Browse files
authored
Merge pull request #1 from ZaparooProject/mifare-clone-quirks
Support for mifare clones
2 parents 0060e1d + 1c35c6e commit 37c4e90

File tree

3 files changed

+660
-12
lines changed

3 files changed

+660
-12
lines changed

cmd/readtag/main.go

Lines changed: 306 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,50 @@ import (
4040
"github.com/ZaparooProject/go-pn532/transport/uart"
4141
)
4242

43+
// mustPrint prints to stdout, panicking on error (for test output only)
44+
func mustPrint(args ...any) {
45+
_, err := fmt.Print(args...)
46+
if err != nil {
47+
panic(err)
48+
}
49+
}
50+
51+
// mustPrintf prints formatted output to stdout, panicking on error (for test output only)
52+
func mustPrintf(format string, args ...any) {
53+
_, err := fmt.Printf(format, args...)
54+
if err != nil {
55+
panic(err)
56+
}
57+
}
58+
59+
// mustPrintln prints with newline to stdout, panicking on error (for test output only)
60+
func mustPrintln(args ...any) {
61+
_, err := fmt.Println(args...)
62+
if err != nil {
63+
panic(err)
64+
}
65+
}
66+
4367
type config struct {
4468
devicePath *string
4569
timeout *time.Duration
4670
writeText *string
4771
debug *bool
4872
validate *bool
73+
testRobust *bool
74+
testTiming *bool
4975
}
5076

5177
func parseFlags() *config {
5278
cfg := &config{
5379
devicePath: flag.String("device", "",
5480
"Serial device path (e.g., /dev/ttyUSB0 or COM3). Leave empty for auto-detection."),
55-
timeout: flag.Duration("timeout", 30*time.Second, "Timeout for tag detection (default: 30s)"),
56-
writeText: flag.String("write", "", "Text to write to the tag (if not specified, will only read)"),
57-
debug: flag.Bool("debug", false, "Enable debug output"),
58-
validate: flag.Bool("validate", true, "Enable read/write validation (default: true)"),
81+
timeout: flag.Duration("timeout", 30*time.Second, "Timeout for tag detection (default: 30s)"),
82+
writeText: flag.String("write", "", "Text to write to the tag (if not specified, will only read)"),
83+
debug: flag.Bool("debug", false, "Enable debug output"),
84+
validate: flag.Bool("validate", true, "Enable read/write validation (default: true)"),
85+
testRobust: flag.Bool("test-robust", false, "Test robust authentication features for Chinese clone cards"),
86+
testTiming: flag.Bool("test-timing", false, "Test timing variance analysis"),
5987
}
6088
flag.Parse()
6189
return cfg
@@ -192,6 +220,271 @@ func writeTextIfRequested(tag pn532.Tag, writeText string) error {
192220
return nil
193221
}
194222

223+
func testChineseCloneUnlock(mifareTag *pn532.MIFARETag) {
224+
mustPrint("\n=== Testing Chinese Clone Unlock Sequences ===\n")
225+
226+
// Test Gen1 unlock commands directly
227+
unlockCommands := []struct {
228+
name string
229+
desc string
230+
cmd byte
231+
}{
232+
{"Gen1 7-bit", "Chinese Gen1 clone unlock (7-bit UID)", 0x40},
233+
{"Gen1 8-bit", "Chinese Gen1 clone unlock (4-byte UID)", 0x43},
234+
}
235+
236+
foundUnlock := false
237+
238+
for _, unlock := range unlockCommands {
239+
mustPrintf("Trying %s (0x%02X): ", unlock.name, unlock.cmd)
240+
241+
// Access the device directly to test unlock commands
242+
device := mifareTag.GetDevice() // We'll need to add this method
243+
if device == nil {
244+
mustPrintln("FAILED - cannot access device")
245+
continue
246+
}
247+
248+
// Try the unlock command
249+
start := time.Now()
250+
_, err := device.SendDataExchange([]byte{unlock.cmd})
251+
duration := time.Since(start)
252+
253+
if err == nil {
254+
mustPrintf("SUCCESS (%.2fms) - %s\n", float64(duration.Nanoseconds())/1000000, unlock.desc)
255+
foundUnlock = true
256+
257+
// If unlock successful, try to read manufacturer block directly
258+
mustPrintln(" Attempting direct block 0 read (no authentication needed)...")
259+
if data, readErr := mifareTag.ReadBlockDirect(0); readErr == nil {
260+
mustPrintf(" Block 0 (UID): %02X %02X %02X %02X %02X %02X %02X %02X...\n",
261+
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7])
262+
mustPrintln(" → This is a Gen1 Chinese clone with backdoor access!")
263+
} else {
264+
mustPrintf(" Block 0 read failed: %v\n", readErr)
265+
}
266+
} else {
267+
mustPrintf("FAILED (%.2fms) - %v\n", float64(duration.Nanoseconds())/1000000, err)
268+
}
269+
}
270+
271+
if !foundUnlock {
272+
mustPrintln("\nNo Chinese clone unlock sequences successful.")
273+
mustPrintln("This may be a Gen2/CUID/FUID clone or genuine card.")
274+
275+
// Test FM11RF08S universal backdoor key
276+
mustPrint("\nTesting FM11RF08S universal backdoor key: ")
277+
backdoorKey := []byte{0xA3, 0x96, 0xEF, 0xA4, 0xE2, 0x4F}
278+
279+
start := time.Now()
280+
err := mifareTag.AuthenticateRobust(1, pn532.MIFAREKeyA, backdoorKey)
281+
duration := time.Since(start)
282+
283+
if err == nil {
284+
mustPrintf("SUCCESS (%.2fms)\n", float64(duration.Nanoseconds())/1000000)
285+
mustPrintln("→ This is likely an FM11RF08S clone with universal backdoor!")
286+
} else {
287+
mustPrintf("FAILED (%.2fms) - %v\n", float64(duration.Nanoseconds())/1000000, err)
288+
mustPrintln("→ Backdoor key authentication failed")
289+
}
290+
}
291+
}
292+
293+
// tryKeyOnSector attempts to authenticate with a given key and sector
294+
func tryKeyOnSector(
295+
mifareTag *pn532.MIFARETag,
296+
sector uint8,
297+
key []byte,
298+
keyType byte,
299+
keyName string,
300+
) (success bool, duration time.Duration) {
301+
mustPrintf(" Trying Key %s [%02X %02X %02X %02X %02X %02X]: ",
302+
keyName, key[0], key[1], key[2], key[3], key[4], key[5])
303+
304+
start := time.Now()
305+
err := mifareTag.AuthenticateRobust(sector, keyType, key)
306+
duration = time.Since(start)
307+
308+
if err == nil {
309+
mustPrintf("SUCCESS (%.2fms)\n", float64(duration.Nanoseconds())/1000000)
310+
testSectorRead(mifareTag, sector)
311+
return true, duration
312+
}
313+
314+
mustPrintf("FAILED (%.2fms) - %v\n", float64(duration.Nanoseconds())/1000000, err)
315+
analysis := mifareTag.AnalyzeLastError(err)
316+
mustPrintf(" Error analysis: %s\n", analysis)
317+
return false, duration
318+
}
319+
320+
// testSectorRead attempts to read a block from the authenticated sector
321+
func testSectorRead(mifareTag *pn532.MIFARETag, sector uint8) {
322+
block := sector * 4 // First block of sector
323+
if data, readErr := mifareTag.ReadBlock(block); readErr == nil {
324+
mustPrintf(" Block %d read: %02X %02X %02X ... (16 bytes)\n",
325+
block, data[0], data[1], data[2])
326+
} else {
327+
mustPrintf(" Block %d read failed: %v\n", block, readErr)
328+
analysis := mifareTag.AnalyzeLastError(readErr)
329+
mustPrintf(" Error analysis: %s\n", analysis)
330+
}
331+
}
332+
333+
// testSectorAuthentication tests all keys for a given sector
334+
func testSectorAuthentication(
335+
mifareTag *pn532.MIFARETag,
336+
sector uint8,
337+
testKeys [][]byte,
338+
) (success bool, authCount int) {
339+
mustPrintf("\nTesting sector %d:\n", sector)
340+
341+
for _, key := range testKeys {
342+
for _, keyType := range []byte{pn532.MIFAREKeyA, pn532.MIFAREKeyB} {
343+
keyName := "A"
344+
if keyType == pn532.MIFAREKeyB {
345+
keyName = "B"
346+
}
347+
348+
if keySuccess, _ := tryKeyOnSector(mifareTag, sector, key, keyType, keyName); keySuccess {
349+
return true, 1
350+
}
351+
authCount++
352+
}
353+
}
354+
355+
mustPrintf(" No working keys found for sector %d\n", sector)
356+
return false, authCount
357+
}
358+
359+
func testRobustAuthentication(tag pn532.Tag) {
360+
mifareTag, ok := tag.(*pn532.MIFARETag)
361+
if !ok {
362+
mustPrintln("Tag is not a MIFARE tag, skipping robust authentication test")
363+
return
364+
}
365+
366+
mustPrint("\n=== Testing Robust Authentication ===\n")
367+
368+
testChineseCloneUnlock(mifareTag)
369+
370+
testKeys := [][]byte{
371+
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // Default transport key
372+
{0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}, // NDEF key
373+
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // All zeros
374+
{0xA3, 0x96, 0xEF, 0xA4, 0xE2, 0x4F}, // FM11RF08S universal backdoor key
375+
}
376+
377+
successfulAuths := 0
378+
totalAttempts := 0
379+
380+
for sector := uint8(1); sector < 4; sector++ {
381+
success, attempts := testSectorAuthentication(mifareTag, sector, testKeys)
382+
if success {
383+
successfulAuths++
384+
}
385+
totalAttempts += attempts
386+
}
387+
388+
mustPrint("\nAuthentication Summary:\n")
389+
mustPrintf(" Successful: %d/%d (%.1f%%)\n",
390+
successfulAuths, totalAttempts, float64(successfulAuths*100)/float64(totalAttempts))
391+
392+
variance := mifareTag.GetTimingVariance()
393+
mustPrintf(" Timing variance: %.2fms\n", float64(variance.Nanoseconds())/1000000)
394+
395+
if mifareTag.IsTimingUnstable() {
396+
mustPrintln(" WARNING: High timing variance detected - possible hardware issues")
397+
} else {
398+
mustPrintln(" Timing appears stable")
399+
}
400+
}
401+
402+
// performTimingAttempts runs authentication attempts and collects timing data
403+
func performTimingAttempts(
404+
mifareTag *pn532.MIFARETag,
405+
sector uint8,
406+
key []byte,
407+
attempts int,
408+
) (timings []time.Duration, successCount int) {
409+
timings = make([]time.Duration, 0, attempts)
410+
successCount = 0
411+
412+
for i := 0; i < attempts; i++ {
413+
start := time.Now()
414+
err := mifareTag.AuthenticateRobust(sector, pn532.MIFAREKeyA, key)
415+
duration := time.Since(start)
416+
timings = append(timings, duration)
417+
418+
if err == nil {
419+
successCount++
420+
mustPrintf(" Attempt %2d: SUCCESS (%.2fms)\n", i+1, float64(duration.Nanoseconds())/1000000)
421+
} else {
422+
mustPrintf(" Attempt %2d: FAILED (%.2fms) - %v\n", i+1, float64(duration.Nanoseconds())/1000000, err)
423+
}
424+
425+
time.Sleep(100 * time.Millisecond)
426+
}
427+
428+
return timings, successCount
429+
}
430+
431+
// calculateTimingStats computes and displays timing statistics
432+
func calculateTimingStats(timings []time.Duration, successCount, attempts int) {
433+
if len(timings) == 0 {
434+
return
435+
}
436+
437+
var minTime, maxTime, total time.Duration = timings[0], timings[0], 0
438+
for _, timing := range timings {
439+
if timing < minTime {
440+
minTime = timing
441+
}
442+
if timing > maxTime {
443+
maxTime = timing
444+
}
445+
total += timing
446+
}
447+
448+
avg := total / time.Duration(len(timings))
449+
variance := maxTime - minTime
450+
451+
mustPrint("\nTiming Statistics:\n")
452+
mustPrintf(" Success rate: %d/%d (%.1f%%)\n",
453+
successCount, attempts, float64(successCount*100)/float64(attempts))
454+
mustPrintf(" Min time: %.2fms\n", float64(minTime.Nanoseconds())/1000000)
455+
mustPrintf(" Max time: %.2fms\n", float64(maxTime.Nanoseconds())/1000000)
456+
mustPrintf(" Avg time: %.2fms\n", float64(avg.Nanoseconds())/1000000)
457+
mustPrintf(" Variance: %.2fms\n", float64(variance.Nanoseconds())/1000000)
458+
459+
switch {
460+
case variance > 1000*time.Millisecond:
461+
mustPrintln(" WARNING: High variance (>1000ms) indicates possible hardware issues")
462+
case variance > 500*time.Millisecond:
463+
mustPrintln(" CAUTION: Moderate variance detected")
464+
default:
465+
mustPrintln(" Timing appears stable")
466+
}
467+
}
468+
469+
func testTimingAnalysis(tag pn532.Tag) {
470+
mifareTag, ok := tag.(*pn532.MIFARETag)
471+
if !ok {
472+
mustPrintln("Tag is not a MIFARE tag, skipping timing analysis test")
473+
return
474+
}
475+
476+
mustPrint("\n=== Testing Timing Analysis ===\n")
477+
478+
key := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}
479+
sector := uint8(1)
480+
attempts := 10
481+
482+
mustPrintf("Performing %d authentication attempts to sector %d...\n", attempts, sector)
483+
484+
timings, successCount := performTimingAttempts(mifareTag, sector, key, attempts)
485+
calculateTimingStats(timings, successCount, attempts)
486+
}
487+
195488
func main() {
196489
cfg := parseFlags()
197490

@@ -215,5 +508,14 @@ func main() {
215508
return
216509
}
217510

511+
// Run tests if requested
512+
if *cfg.testRobust {
513+
testRobustAuthentication(tag)
514+
}
515+
516+
if *cfg.testTiming {
517+
testTimingAnalysis(tag)
518+
}
519+
218520
_, _ = fmt.Print(tag.DebugInfo())
219521
}

0 commit comments

Comments
 (0)