-
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmifare.go
More file actions
1519 lines (1287 loc) · 43 KB
/
mifare.go
File metadata and controls
1519 lines (1287 loc) · 43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2026 The Zaparoo Project Contributors.
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pn532
import (
"bytes"
"context"
"crypto/rand"
"errors"
"fmt"
"math/big"
"strings"
"time"
"github.com/ZaparooProject/go-pn532/internal/syncutil"
)
// By default we only support MIFARE Classic tags with NDEF formatted data
// which uses a pre-shared standard auth key:
// [0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7]
//
// This key is used on sector 1 and/or greater. Sector 0 is reserved for the
// MAD (MIFARE Application Directory) and uses a different shared key, but we
// don't care about implementing this.
//
// Additionally that means we should only use sector 1 and above for reading
// and writing our own data.
//
// MIFARE Classic tags may ship blank using the default key:
// [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]
//
// Before they work with NDEF data, the tag must also be intialized to use
// the standard NDEF auth key.
var (
// NDEF standard key for sector 1 and above
ndefKeyTemplate = []byte{0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}
// Common alternative keys to try
commonKeys = [][]byte{
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // Default transport key
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // All zeros
{0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}, // MAD key
{0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5}, // Common alternative
{0xA3, 0x96, 0xEF, 0xA4, 0xE2, 0x4F}, // FM11RF08S universal backdoor key
}
// Chinese clone unlock commands
chineseCloneUnlock7Bit = byte(0x40)
chineseCloneUnlock8Bit = byte(0x43)
)
// MIFARE commands
const (
mifareCmdAuth = 0x60
mifareCmdRead = 0x30
mifareCmdWrite = 0xA0
)
// MIFARE memory structure
const (
mifareBlockSize = 16 // 16 bytes per block
mifareSectorSize = 4 // 4 blocks per sector
mifareManufacturerBlock = 0 // Manufacturer block
mifareKeySize = 6 // 6 bytes per key
)
// Key types
const (
MIFAREKeyA = 0x00
MIFAREKeyB = 0x01
)
// Retry levels (progressive recovery)
type retryLevel int
const (
retryLight retryLevel = iota // Simple retry with delay
retryModerate // Halt/wake sequence
retryHeavy // RF field reset
retryNuclear // Complete PN532 reinitialization
)
// Authentication timing metrics
type authTiming struct {
attempts []time.Duration
mutex syncutil.RWMutex
}
func (at *authTiming) add(duration time.Duration) {
at.mutex.Lock()
defer at.mutex.Unlock()
at.attempts = append(at.attempts, duration)
if len(at.attempts) > 20 {
at.attempts = at.attempts[1:]
}
}
func (at *authTiming) getVariance() time.Duration {
at.mutex.RLock()
defer at.mutex.RUnlock()
if len(at.attempts) < 2 {
return 0
}
minVal, maxVal := at.attempts[0], at.attempts[0]
for _, d := range at.attempts[1:] {
if d < minVal {
minVal = d
}
if d > maxVal {
maxVal = d
}
}
return maxVal - minVal
}
// secureKey manages MIFARE keys with automatic zeroing
type secureKey struct {
data [6]byte
}
// newSecureKey creates a secure key from template
func newSecureKey(template []byte) *secureKey {
if len(template) != 6 {
return nil
}
sk := &secureKey{}
copy(sk.data[:], template)
return sk
}
// bytes returns a copy of the key data (caller must zero it)
func (sk *secureKey) bytes() []byte {
result := make([]byte, 6)
copy(result, sk.data[:])
return result
}
// MIFAREConfig holds all configurable timing parameters for MIFARE operations
type MIFAREConfig struct {
RetryConfig *RetryConfig // Retry backoff configuration
HardwareDelay time.Duration // Hardware timing delays (reinitialization, tag processing)
}
// DefaultMIFAREConfig returns production-safe MIFARE configuration
// These values are optimized based on real-world testing and provide
// a good balance between speed and reliability for most hardware setups.
func DefaultMIFAREConfig() *MIFAREConfig {
return &MIFAREConfig{
RetryConfig: &RetryConfig{
MaxAttempts: 3,
InitialBackoff: 10 * time.Millisecond,
MaxBackoff: 1 * time.Second,
BackoffMultiplier: 2.0,
Jitter: 0.1,
RetryTimeout: 5 * time.Second,
},
HardwareDelay: 10 * time.Millisecond,
}
}
// MIFARETag represents a MIFARE Classic tag
type MIFARETag struct {
ndefKey *secureKey
config *MIFAREConfig
BaseTag
timing authTiming
lastAuthSector int
authMutex syncutil.RWMutex
lastAuthKeyType byte
authenticated bool // true if TryAuthenticate succeeded
}
// NewMIFARETag creates a new MIFARE tag instance
func NewMIFARETag(device *Device, uid []byte, sak byte) *MIFARETag {
tag := &MIFARETag{
ndefKey: newSecureKey(ndefKeyTemplate),
BaseTag: BaseTag{
tagType: TagTypeMIFARE,
uid: uid,
device: device,
sak: sak,
},
lastAuthSector: -1, // Not authenticated initially
config: DefaultMIFAREConfig(),
}
return tag
}
// SetConfig allows runtime configuration of MIFARE behavior for testing
func (t *MIFARETag) SetConfig(config *MIFAREConfig) {
if config != nil {
t.config = config
}
}
// SetRetryConfig allows runtime configuration of retry behavior for testing
func (t *MIFARETag) SetRetryConfig(config *RetryConfig) {
if config != nil {
t.config.RetryConfig = config
}
}
// authenticateWithNDEFKey authenticates to a sector using NDEF standard key with robust retry.
// This only tries the NDEF key - use authenticateWithKeyFallback if you need to try common keys.
func (t *MIFARETag) authenticateWithNDEFKey(ctx context.Context, sector uint8, keyType byte) error {
if err := ctx.Err(); err != nil {
return err
}
if t.ndefKey == nil {
return errors.New("NDEF key not available")
}
key := t.ndefKey.bytes()
err := t.AuthenticateWithRetry(ctx, sector, keyType, key)
// SECURITY: Zero key copy after use
for i := range key {
key[i] = 0
}
return err
}
// authenticateForSectorRead tries Key A then Key B for read operations
func (t *MIFARETag) authenticateForSectorRead(ctx context.Context, sector uint8) error {
err := t.authenticateWithNDEFKey(ctx, sector, MIFAREKeyA)
if err == nil {
return nil
}
// Re-select tag before trying Key B - failed auth leaves tag in HALT state
if reselectErr := t.quickReselect(ctx); isTransportLockup(reselectErr) {
return reselectErr
}
if err = t.authenticateWithNDEFKey(ctx, sector, MIFAREKeyB); err != nil {
return fmt.Errorf("failed to authenticate to sector %d: %w", sector, err)
}
return nil
}
// ReadBlockAuto reads a block with automatic authentication using the key provider
func (t *MIFARETag) ReadBlockAuto(ctx context.Context, block uint8) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
sector := block / mifareSectorSize
// SECURITY: Thread-safe authentication state checking
t.authMutex.RLock()
needAuth := t.lastAuthSector != int(sector)
t.authMutex.RUnlock()
if needAuth {
if err := t.authenticateForSectorRead(ctx, sector); err != nil {
return nil, err
}
}
return t.ReadBlock(ctx, block)
}
// WriteBlockAuto writes a block with automatic authentication using the key provider
func (t *MIFARETag) WriteBlockAuto(ctx context.Context, block uint8, data []byte) error {
if err := ctx.Err(); err != nil {
return err
}
sector := block / mifareSectorSize
// Already authenticated to this sector - proceed with write
if t.lastAuthSector == int(sector) {
return t.WriteBlock(ctx, block, data)
}
// Need to authenticate to a different sector
// Clear any stale PN532 auth state before switching sectors
if t.lastAuthSector >= 0 {
if err := t.ResetAuthState(ctx); isTransportLockup(err) {
return err
}
}
// For write operations, typically Key B is required (but this depends on access bits)
// Try Key B first, then Key A
if err := t.authenticateWithNDEFKey(ctx, sector, MIFAREKeyB); err != nil {
// Re-select tag before trying Key A - failed auth leaves tag in HALT state
if reselectErr := t.quickReselect(ctx); isTransportLockup(reselectErr) {
return reselectErr
}
if err := t.authenticateWithNDEFKey(ctx, sector, MIFAREKeyA); err != nil {
return fmt.Errorf("failed to authenticate to sector %d: %w", sector, err)
}
}
return t.WriteBlock(ctx, block, data)
}
// ReadBlock reads a block from the MIFARE tag
func (t *MIFARETag) ReadBlock(ctx context.Context, block uint8) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
// Check if we need to authenticate to this sector
sector := int(block / mifareSectorSize)
t.authMutex.RLock()
authenticated := t.lastAuthSector == sector
t.authMutex.RUnlock()
if !authenticated {
return nil, fmt.Errorf("not authenticated to sector %d (block %d)", sector, block)
}
// Send read command with retry on timeout
data, err := t.device.SendDataExchangeWithRetry(ctx, []byte{mifareCmdRead, block})
if err != nil {
return nil, fmt.Errorf("%w (block %d): %w", ErrTagReadFailed, block, err)
}
// MIFARE Classic returns 16 bytes on read
if len(data) < mifareBlockSize {
return nil, fmt.Errorf("%w: invalid response length %d (expected at least %d)",
ErrTagReadFailed, len(data), mifareBlockSize)
}
return data[:mifareBlockSize], nil
}
// ReadBlockDirect reads a block directly without authentication (for clone tags).
func (t *MIFARETag) ReadBlockDirect(ctx context.Context, block uint8) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
// Send read command with retry on timeout
data, err := t.device.SendDataExchangeWithRetry(ctx, []byte{mifareCmdRead, block})
if err != nil {
// If we still get a timeout error after retries, try InCommunicateThru as fallback
if IsPN532TimeoutError(err) {
return t.readBlockCommunicateThru(ctx, block)
}
return nil, fmt.Errorf("%w (block %d): %w", ErrTagReadFailed, block, err)
}
// MIFARE Classic returns 16 bytes on read
if len(data) < mifareBlockSize {
return nil, fmt.Errorf("%w: invalid response length %d (expected at least %d)",
ErrTagReadFailed, len(data), mifareBlockSize)
}
return data[:mifareBlockSize], nil
}
// WriteBlock writes a block to the MIFARE tag
func (t *MIFARETag) WriteBlock(ctx context.Context, block uint8, data []byte) error {
if err := ctx.Err(); err != nil {
return err
}
// Validate data size
if len(data) != mifareBlockSize {
return fmt.Errorf("invalid block size: expected %d, got %d", mifareBlockSize, len(data))
}
// Check if we need to authenticate to this sector
sector := int(block / mifareSectorSize)
if t.lastAuthSector != sector {
return fmt.Errorf("not authenticated to sector %d (block %d)", sector, block)
}
// Don't allow writing to manufacturer block
if block == mifareManufacturerBlock {
return errors.New("cannot write to manufacturer block")
}
// Send write command
cmd := make([]byte, 0, 2+len(data))
cmd = append(cmd, mifareCmdWrite, block)
cmd = append(cmd, data...)
_, err := t.device.SendDataExchangeWithRetry(ctx, cmd)
if err != nil {
return fmt.Errorf("%w (block %d): %w", ErrTagWriteFailed, block, err)
}
return nil
}
// WriteBlockDirect writes a block directly without authentication (for clone tags)
func (t *MIFARETag) WriteBlockDirect(ctx context.Context, block uint8, data []byte) error {
if err := ctx.Err(); err != nil {
return err
}
// Validate data size
if len(data) != mifareBlockSize {
return fmt.Errorf("invalid block size: expected %d, got %d", mifareBlockSize, len(data))
}
// Don't allow writing to manufacturer block
if block == mifareManufacturerBlock {
return errors.New("cannot write to manufacturer block")
}
// First, try to read the block to see if the tag is responsive
_, err := t.ReadBlockDirect(ctx, block)
if err != nil {
// If we can't even read, the tag might not support direct access at all
return fmt.Errorf("clone tag does not support direct block access: %w", err)
}
// Send write command directly
cmd := make([]byte, 0, 2+len(data))
cmd = append(cmd, mifareCmdWrite, block)
cmd = append(cmd, data...)
_, err = t.device.SendDataExchangeWithRetry(ctx, cmd)
if err != nil {
// Try alternative approach - some clones might need different handling
return t.writeBlockDirectAlternative(ctx, block, data, err)
}
return nil
}
// writeBlockDirectAlternative tries alternative methods for clone tags that don't respond to
// standard writes
func (t *MIFARETag) writeBlockDirectAlternative(
ctx context.Context, block uint8, data []byte, originalErr error,
) error {
// Check if the original error was a timeout (0x01)
if IsPN532TimeoutError(originalErr) {
// Try using InCommunicateThru instead of InDataExchange
// Some clone tags might respond better to raw communication
err := t.writeBlockCommunicateThru(ctx, block, data)
if err == nil {
return nil
}
// If InCommunicateThru also fails, this tag may not support writing
return errors.New("tag does not support writing: this tag may be read-only or have limited write functionality")
}
// For other errors, return the original error
return fmt.Errorf("%w (block %d): %w", ErrTagWriteFailed, block, originalErr)
}
// writeBlockCommunicateThru tries to write a block using InCommunicateThru instead of InDataExchange
func (t *MIFARETag) writeBlockCommunicateThru(ctx context.Context, block uint8, data []byte) error {
if err := ctx.Err(); err != nil {
return err
}
// Validate data size
if len(data) != mifareBlockSize {
return fmt.Errorf("invalid block size: expected %d, got %d", mifareBlockSize, len(data))
}
// Don't allow writing to manufacturer block
if block == mifareManufacturerBlock {
return errors.New("cannot write to manufacturer block")
}
// Build MIFARE write command
cmd := make([]byte, 0, 2+len(data))
cmd = append(cmd, mifareCmdWrite, block)
cmd = append(cmd, data...)
// Use SendRawCommand instead of SendDataExchange
_, err := t.device.SendRawCommand(ctx, cmd)
// Re-select target after SendRawCommand to restore PN532 internal state
if selectErr := t.device.InSelect(ctx); selectErr != nil {
Debugln("MIFARE writeBlockCommunicateThru: InSelect failed:", selectErr)
}
if err != nil {
return fmt.Errorf("raw write command failed: %w", err)
}
return nil
}
// readBlockCommunicateThru tries to read a block using InCommunicateThru instead of InDataExchange
func (t *MIFARETag) readBlockCommunicateThru(ctx context.Context, block uint8) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
// Build MIFARE read command
cmd := []byte{mifareCmdRead, block}
// Use SendRawCommand instead of SendDataExchange
data, err := t.device.SendRawCommand(ctx, cmd)
// Re-select target after SendRawCommand to restore PN532 internal state
if selectErr := t.device.InSelect(ctx); selectErr != nil {
Debugln("MIFARE readBlockCommunicateThru: InSelect failed:", selectErr)
}
if err != nil {
return nil, fmt.Errorf("raw read command failed: %w", err)
}
// MIFARE Classic returns 16 bytes on read
if len(data) < mifareBlockSize {
return nil, fmt.Errorf("%w: invalid response length %d (expected at least %d)",
ErrTagReadFailed, len(data), mifareBlockSize)
}
return data[:mifareBlockSize], nil
}
// ReadNDEFWithRetry reads NDEF data with retry logic to handle intermittent empty data issues
// This addresses the "empty valid tag" problem where tags are detected but return no data
func (t *MIFARETag) ReadNDEFWithRetry(ctx context.Context) (*NDEFMessage, error) {
return readNDEFWithRetry(func() (*NDEFMessage, error) {
return t.ReadNDEF(ctx)
}, isMifareRetryableError, "MIFARE")
}
// isMifareRetryableError determines if a MIFARE error is worth retrying
func isMifareRetryableError(err error) bool {
if err == nil {
return false
}
// Never retry on transport lockup - device needs hard reset
if isTransportLockup(err) {
return false
}
// Use the centralized retry logic from errors.go
// This handles authentication failures, timeouts, read failures, and communication errors
return IsRetryable(err) ||
errors.Is(err, ErrTagAuthFailed) ||
errors.Is(err, ErrTagReadFailed)
}
// ReadNDEF reads NDEF data from the MIFARE tag using bulk sector reads
func (t *MIFARETag) ReadNDEF(ctx context.Context) (*NDEFMessage, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
maxSectors, initialCapacity := t.getTagCapacityParams()
data := make([]byte, 0, initialCapacity)
for sector := uint8(1); sector < maxSectors; sector++ {
if err := ctx.Err(); err != nil {
return nil, err
}
if err := t.authenticateSector(ctx, sector); err != nil {
if sector == 1 {
return t.handleSector1AuthError(err)
}
break
}
sectorData, foundEnd := t.readSectorData(ctx, sector)
if len(sectorData) > 0 {
data = append(data, sectorData...)
}
readState := ndefReadContinue
if foundEnd {
readState = ndefReadFoundEnd
}
if t.shouldStopReading(sectorData, readState, data) {
break
}
}
return ParseNDEFMessage(data)
}
func (t *MIFARETag) getTagCapacityParams() (maxSectors uint8, initialCapacity int) {
if t.IsMIFARE4K() {
return 40, 255 * mifareBlockSize // 4K tag has 40 sectors
}
return 16, 64 * mifareBlockSize // 1K tag has 16 sectors
}
func (t *MIFARETag) authenticateSector(ctx context.Context, sector uint8) error {
// Try to authenticate to the sector with Key A
if err := t.authenticateWithNDEFKey(ctx, sector, MIFAREKeyA); err != nil {
// Try Key B if Key A failed
return t.authenticateWithNDEFKey(ctx, sector, MIFAREKeyB)
}
return nil
}
// handleSector1AuthError determines how to handle an authentication error on sector 1.
// Communication errors propagate; auth failures (wrong key) mean the tag isn't NDEF formatted.
func (*MIFARETag) handleSector1AuthError(err error) (*NDEFMessage, error) {
if isCommunicationError(err) {
return nil, fmt.Errorf("%w: %w", ErrTagReadFailed, err)
}
// Auth failure means tag doesn't use NDEF key - return empty NDEF
// (consistent with NTAG behavior for non-NDEF formatted tags)
return &NDEFMessage{}, nil
}
type ndefReadState int
const (
ndefReadContinue ndefReadState = iota
ndefReadFoundEnd
)
func (*MIFARETag) shouldStopReading(sectorData []byte, readState ndefReadState, allData []byte) bool {
// Check if we found the NDEF end marker
if readState == ndefReadFoundEnd || bytes.Contains(allData, ndefEnd) {
return true
}
// Stop if sector was empty
return len(sectorData) == 0 || isEmptyData(sectorData)
}
// readSectorData reads all data blocks in a sector (excluding the trailer)
// Returns the data and whether an NDEF end marker was found
func (t *MIFARETag) readSectorData(ctx context.Context, sector uint8) ([]byte, bool) {
startBlock := sector * mifareSectorSize
endBlock := startBlock + mifareSectorSize - 1 // -1 to exclude trailer
data := make([]byte, 0, (mifareSectorSize-1)*mifareBlockSize)
foundEnd := false
for block := startBlock; block < endBlock; block++ {
if ctx.Err() != nil {
break
}
blockData, err := t.ReadBlock(ctx, block)
if err != nil {
break
}
data = append(data, blockData...)
// Check for NDEF end marker
if bytes.Contains(blockData, ndefEnd) {
foundEnd = true
break
}
}
return data, foundEnd
}
// isEmptyData checks if the data is all zeros
func isEmptyData(data []byte) bool {
for _, b := range data {
if b != 0 {
return false
}
}
return true
}
// WriteNDEF writes NDEF data to the MIFARE tag with final verification
func (t *MIFARETag) WriteNDEF(ctx context.Context, message *NDEFMessage) error {
if err := ctx.Err(); err != nil {
return err
}
if len(message.Records) == 0 {
return errors.New("no NDEF records to write")
}
data, err := BuildNDEFMessageEx(message.Records)
if err != nil {
return fmt.Errorf("failed to build NDEF message: %w", err)
}
authResult, err := t.authenticateWithKeyFallback(ctx)
if err != nil {
return err
}
if err := t.validateNDEFSize(data); err != nil {
return err
}
if authResult.isBlank {
if err := t.formatForNDEFWithKey(ctx, authResult.blankKey); err != nil {
return fmt.Errorf("failed to format tag for NDEF: %w", err)
}
}
if err := ctx.Err(); err != nil {
return err
}
if err := t.writeNDEFData(ctx, data); err != nil {
return err
}
if err := t.clearRemainingBlocks(ctx, t.calculateNextBlock(len(data))); err != nil {
return err
}
if err := ctx.Err(); err != nil {
return err
}
// Verify write by reading back and comparing
return t.verifyWrittenNDEFData(ctx, data)
}
type authenticationResult struct {
blankKey []byte
isBlank bool
isNDEFFormatted bool
}
// quickReselect attempts to re-select the tag after a failed authentication.
// After failed MIFARE auth, the tag enters HALT state and won't respond to REQA.
// Uses InDeselect (keeps target info) + InSelect (uses WUPA to wake HALTed tags).
func (t *MIFARETag) quickReselect(ctx context.Context) error {
Debugln("quickReselect: starting")
// InDeselect sends HLTA but keeps target info in PN532 memory
// This is different from InRelease which clears target info
if err := t.device.InDeselect(ctx); err != nil {
Debugf("quickReselect: InDeselect failed: %v", err)
// Continue anyway - InSelect may still work
} else {
Debugln("quickReselect: InDeselect succeeded")
}
// InSelect uses WUPA (not REQA) to wake HALTed tags
// This works because InDeselect preserved the target info
if err := t.device.InSelect(ctx); err != nil {
Debugf("quickReselect: InSelect failed: %v", err)
return err
}
Debugln("quickReselect: InSelect succeeded")
return nil
}
// tryAuthWithBothKeys attempts authentication with both Key A and Key B.
// Uses authenticateOnce for fast probing during init (single attempt per key).
// Returns (true, nil) if either key succeeds, (false, nil) if auth fails normally,
// or (false, err) if a transport lockup occurs.
func (t *MIFARETag) tryAuthWithBothKeys(ctx context.Context, sector uint8, key []byte) (bool, error) {
err := t.authenticateOnce(ctx, sector, MIFAREKeyA, key)
if err == nil {
return true, nil
}
if isTransportLockup(err) {
return false, err
}
// Check if context was cancelled before trying Key B
// This prevents long waits when tag was removed
if ctxErr := ctx.Err(); ctxErr != nil {
return false, ctxErr
}
// authenticateOnce does quickReselect on failure, so tag should be ready
err = t.authenticateOnce(ctx, sector, MIFAREKeyB, key)
if err == nil {
return true, nil
}
if isTransportLockup(err) {
return false, err
}
return false, nil
}
// TryAuthenticate attempts to authenticate to sector 1 using NDEF key first,
// then falling back to common keys (factory default, etc.).
// Returns nil if any key works, error otherwise.
// Use this when initializing or checking if a tag can be read.
func (t *MIFARETag) TryAuthenticate(ctx context.Context) error {
_, err := t.authenticateWithKeyFallback(ctx)
if err == nil {
t.authenticated = true
}
return err
}
// IsAuthenticated returns true if TryAuthenticate succeeded.
// Unauthenticated tags can still be used for UID-only operations.
func (t *MIFARETag) IsAuthenticated() bool {
return t.authenticated
}
// authenticateWithKeyFallback tries to authenticate to sector 1, first with the NDEF key,
// then falling back to common keys (factory default, etc.). Returns info about which key worked.
// Use this when initializing or when the tag's key is unknown.
// Uses fast single-attempt probing (authenticateOnce) for speed during init.
// Tries Chinese clone unlock once at the end if all keys fail.
func (t *MIFARETag) authenticateWithKeyFallback(ctx context.Context) (*authenticationResult, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
// First try NDEF key for already-formatted tags to avoid state corruption
ndefKeyBytes := t.ndefKey.bytes()
defer clearKey(ndefKeyBytes)
ok, err := t.tryAuthWithBothKeys(ctx, 1, ndefKeyBytes)
if err != nil {
return nil, err
}
if ok {
return &authenticationResult{isNDEFFormatted: true}, nil
}
// If NDEF key failed, try common keys for blank tags
for _, key := range commonKeys {
if err := ctx.Err(); err != nil {
return nil, err
}
ok, err := t.tryAuthWithBothKeys(ctx, 1, key)
if err != nil {
return nil, err
}
if ok {
return &authenticationResult{isBlank: true, blankKey: key}, nil
}
}
// If all standard keys failed, try Chinese clone unlock once
// This handles Gen1 clone tags that don't require authentication
if ctx.Err() == nil {
ok, err := t.tryChineseCloneUnlock(ctx, 1)
if err != nil {
return nil, err
}
if ok {
// Clone unlock successful - mark as authenticated
t.authMutex.Lock()
t.lastAuthSector = 1
t.lastAuthKeyType = MIFAREKeyA
t.authMutex.Unlock()
return &authenticationResult{isBlank: true}, nil
}
}
return nil, errors.New("cannot authenticate to tag - it may use custom keys, be protected, " +
"or be a non-standard tag. Supported keys: default (blank), NDEF standard")
}
// clearKey securely clears sensitive key data.
func clearKey(key []byte) {
for i := range key {
key[i] = 0
}
}
func (t *MIFARETag) validateNDEFSize(data []byte) error {
// Determine max blocks based on card type
var maxBlocks int
if t.IsMIFARE4K() {
maxBlocks = 255 // 4K card has 255 blocks (0-254)
} else {
maxBlocks = 64 // 1K card has 64 blocks (0-63)
}
dataBlocks := 0
for i := 4; i < maxBlocks; i++ {
if i%4 != 3 { // Skip sector trailers
dataBlocks++
}
}
maxDataSize := dataBlocks * mifareBlockSize
if len(data) > maxDataSize {
return fmt.Errorf("NDEF message too large: %d bytes, max %d bytes", len(data), maxDataSize)
}
return nil
}
func (t *MIFARETag) writeNDEFData(ctx context.Context, data []byte) error {
// Determine max blocks based on card type
var maxBlocks uint8
if t.IsMIFARE4K() {
maxBlocks = 255 // 4K card has 255 blocks (0-254)
} else {
maxBlocks = 64 // 1K card has 64 blocks (0-63)
}
block := uint8(4)
for i := 0; i < len(data); i += mifareBlockSize {
if ctx.Err() != nil {
return ctx.Err()
}
if block%4 == 3 {
block++
}
if block >= maxBlocks {
return errors.New("NDEF data exceeds tag capacity")
}
if err := t.writeDataBlock(ctx, block, data, i); err != nil {
return err
}
block++
}
return nil
}
func (t *MIFARETag) writeDataBlock(ctx context.Context, block uint8, data []byte, offset int) error {
end := offset + mifareBlockSize
if end > len(data) {
blockData := make([]byte, mifareBlockSize)
copy(blockData, data[offset:])
return t.writeBlockWithError(ctx, block, blockData)
}
return t.writeBlockWithError(ctx, block, data[offset:end])
}
func (t *MIFARETag) writeBlockWithError(ctx context.Context, block uint8, data []byte) error {
if err := t.WriteBlockAuto(ctx, block, data); err != nil {
return fmt.Errorf("%w (block %d): %w", ErrTagWriteFailed, block, err)
}
return nil
}
func (*MIFARETag) calculateNextBlock(dataLen int) uint8 {
block := uint8(4)
for i := 0; i < dataLen; i += mifareBlockSize {
if block%4 == 3 {
block++
}
block++
}
return block
}
// clearRemainingBlocks clears data blocks after NDEF data (best-effort).
// Write failures to non-essential blocks are intentionally ignored.
//
//nolint:nilerr // Intentional: write errors are ignored for best-effort clearing
func (t *MIFARETag) clearRemainingBlocks(ctx context.Context, startBlock uint8) error {
// Determine max blocks based on card type
var maxBlocks uint8
if t.IsMIFARE4K() {
maxBlocks = 255 // 4K card has 255 blocks (0-254)
} else {
maxBlocks = 64 // 1K card has 64 blocks (0-63)
}
block := startBlock
for block < maxBlocks {
if err := ctx.Err(); err != nil {
return err
}
if block%4 == 3 {
block++
continue
}
emptyBlock := make([]byte, mifareBlockSize)
if err := t.WriteBlockAuto(ctx, block, emptyBlock); err != nil {
// It's okay if we can't clear all blocks - this is best effort
break
}
block++
}
return nil
}
// verifyWrittenNDEFData reads back written NDEF data and compares it to the original
func (t *MIFARETag) verifyWrittenNDEFData(ctx context.Context, expectedData []byte) error {
block := uint8(4)
dataOffset := 0
for dataOffset < len(expectedData) {
// Check context before each read
if err := ctx.Err(); err != nil {
return err
}
// Skip sector trailers
if block%4 == 3 {
block++
continue