Release v1.0.3: Enhanced NDEF reading and UX improvements
Major Improvements: - Implement exclusive card access with retry logic and exponential backoff - Fix Type 4 NDEF length parsing (now correctly reads NTAG424 and similar chips) - Add enhanced card type detection (Type 2 vs Type 4) - Implement chunked reading for large NDEF messages - Add proper TLV parsing for Type 2 tags Bug Fixes: - Fix WPF window lifecycle issue (visual tree error on reopen) - Fix NDEF length parsing incorrectly detecting extended format - Correct data offset for Type 4 tag reading New Features: - Multi-line log selection and copy to clipboard - Context menu with Copy Selected Lines, Copy All, Select All - Runtime version roll-forward support (.NET 8.0.x compatibility) Technical Details: - Type 4 tags now use correct 2-byte NLEN field per NFC Forum spec - Removed incorrect 3-byte extended length detection - Window now hides instead of closing for proper tray app behavior - Connection attempts exclusive access first, falls back to shared mode - Status timeout increased from 0ms to 1000ms for better card detection
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using PCSC;
|
||||
using PCSC.Exceptions;
|
||||
|
||||
namespace NfcActions.Services;
|
||||
|
||||
@@ -16,6 +17,9 @@ public class CardReaderService : IDisposable
|
||||
private readonly LogService? _logService;
|
||||
|
||||
private const int POLL_INTERVAL_MS = 500;
|
||||
private const int MAX_RETRY_ATTEMPTS = 3;
|
||||
private const int INITIAL_RETRY_DELAY_MS = 100;
|
||||
private const int STATUS_TIMEOUT_MS = 1000;
|
||||
|
||||
public event EventHandler<ReaderEventArgs>? ReaderAdded;
|
||||
public event EventHandler<ReaderEventArgs>? ReaderRemoved;
|
||||
@@ -150,7 +154,7 @@ public class CardReaderService : IDisposable
|
||||
}
|
||||
};
|
||||
|
||||
var result = context.GetStatusChange(0, readerStates);
|
||||
var result = context.GetStatusChange(STATUS_TIMEOUT_MS, readerStates);
|
||||
|
||||
if (result == SCardError.Success && readerStates.Length > 0)
|
||||
{
|
||||
@@ -215,6 +219,112 @@ public class CardReaderService : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private ICardReader? ConnectReaderWithRetry(ISCardContext context, string readerName)
|
||||
{
|
||||
int retryDelay = INITIAL_RETRY_DELAY_MS;
|
||||
|
||||
for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logService?.Debug($"Attempting to connect to reader (attempt {attempt}/{MAX_RETRY_ATTEMPTS})...");
|
||||
|
||||
// Try exclusive mode first
|
||||
try
|
||||
{
|
||||
var reader = context.ConnectReader(readerName, SCardShareMode.Exclusive, SCardProtocol.Any);
|
||||
_logService?.Debug($"Successfully connected with exclusive access on attempt {attempt}");
|
||||
return reader;
|
||||
}
|
||||
catch (PCSCException ex) when (ex.SCardError == SCardError.SharingViolation && attempt < MAX_RETRY_ATTEMPTS)
|
||||
{
|
||||
_logService?.Debug($"Exclusive access denied (sharing violation), retrying in {retryDelay}ms...");
|
||||
Thread.Sleep(retryDelay);
|
||||
retryDelay *= 2; // Exponential backoff
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logService?.Warning($"Connection attempt {attempt} failed: {ex.Message}");
|
||||
|
||||
if (attempt == MAX_RETRY_ATTEMPTS)
|
||||
{
|
||||
// On final attempt, try shared mode as fallback
|
||||
try
|
||||
{
|
||||
_logService?.Warning("All exclusive access attempts failed, falling back to shared mode...");
|
||||
var reader = context.ConnectReader(readerName, SCardShareMode.Shared, SCardProtocol.Any);
|
||||
_logService?.Warning("Connected with shared access (fallback)");
|
||||
return reader;
|
||||
}
|
||||
catch (Exception fallbackEx)
|
||||
{
|
||||
_logService?.Error($"Failed to connect even with shared mode: {fallbackEx.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
Thread.Sleep(retryDelay);
|
||||
retryDelay *= 2; // Exponential backoff
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private enum NfcCardType
|
||||
{
|
||||
Unknown,
|
||||
Type2, // NTAG, MIFARE Ultralight
|
||||
Type4 // ISO-DEP, DESFire
|
||||
}
|
||||
|
||||
private NfcCardType DetectCardType(ICardReader reader, byte[]? atr)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check ATR for Type 4 indicators
|
||||
if (atr != null && atr.Length > 0)
|
||||
{
|
||||
// Check for ISO 14443-4 support in historical bytes
|
||||
_logService?.Debug($"Analyzing ATR for card type detection: {BitConverter.ToString(atr).Replace("-", " ")}");
|
||||
|
||||
// Type 4 cards typically have longer ATRs with historical bytes
|
||||
if (atr.Length > 10)
|
||||
{
|
||||
_logService?.Debug("Long ATR detected, likely Type 4 card");
|
||||
return NfcCardType.Type4;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to detect card type using protocol
|
||||
if (reader.Protocol == SCardProtocol.T0 || reader.Protocol == SCardProtocol.T1)
|
||||
{
|
||||
_logService?.Debug($"T=0/T=1 protocol detected, likely Type 4 card");
|
||||
// T=0 or T=1 protocols typically indicate Type 4 cards
|
||||
}
|
||||
|
||||
// Try a simple Type 2 read command
|
||||
var testRead = new byte[] { 0x30, 0x00 }; // READ block 0
|
||||
var response = TransmitApdu(reader, testRead, "Type 2 detection probe");
|
||||
if (response != null && response.Length >= 16)
|
||||
{
|
||||
_logService?.Debug("Type 2 READ command successful");
|
||||
return NfcCardType.Type2;
|
||||
}
|
||||
|
||||
// Default to Type 4 for ISO-DEP cards
|
||||
_logService?.Debug("Defaulting to Type 4 card");
|
||||
return NfcCardType.Type4;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logService?.Debug($"Card type detection failed: {ex.Message}");
|
||||
return NfcCardType.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[]? ReadCardData(string readerName)
|
||||
{
|
||||
try
|
||||
@@ -224,7 +334,13 @@ public class CardReaderService : IDisposable
|
||||
using var context = ContextFactory.Instance.Establish(SCardScope.System);
|
||||
|
||||
_logService?.Debug("Connecting to reader...");
|
||||
using var reader = context.ConnectReader(readerName, SCardShareMode.Shared, SCardProtocol.Any);
|
||||
using var reader = ConnectReaderWithRetry(context, readerName);
|
||||
|
||||
if (reader == null)
|
||||
{
|
||||
_logService?.Error("Failed to connect to reader after all retry attempts");
|
||||
return null;
|
||||
}
|
||||
|
||||
_logService?.Debug($"Connected. Active protocol: {reader.Protocol}");
|
||||
|
||||
@@ -235,32 +351,55 @@ public class CardReaderService : IDisposable
|
||||
_logService?.Debug($"ATR: {BitConverter.ToString(atr).Replace("-", " ")}");
|
||||
}
|
||||
|
||||
// Detect card type
|
||||
var cardType = DetectCardType(reader, atr);
|
||||
_logService?.Info($"Detected card type: {cardType}");
|
||||
|
||||
byte[]? ndefData = null;
|
||||
|
||||
// Strategy 1: Try Type 4 Tag (ISO 14443-4 / ISO-DEP)
|
||||
_logService?.Debug("=== Attempting Type 4 Tag NDEF read ===");
|
||||
ndefData = TryReadType4Tag(reader);
|
||||
if (ndefData != null && ndefData.Length > 0)
|
||||
// Use appropriate strategy based on detected card type
|
||||
switch (cardType)
|
||||
{
|
||||
_logService?.Info("Successfully read NDEF data using Type 4 method");
|
||||
return ndefData;
|
||||
case NfcCardType.Type2:
|
||||
_logService?.Debug("=== Reading Type 2 Tag ===");
|
||||
ndefData = TryReadType2TagEnhanced(reader);
|
||||
if (ndefData != null && ndefData.Length > 0)
|
||||
{
|
||||
_logService?.Info("Successfully read NDEF data from Type 2 tag");
|
||||
return ndefData;
|
||||
}
|
||||
// Fallback to Type 4 if Type 2 fails
|
||||
_logService?.Debug("Type 2 read failed, trying Type 4 as fallback");
|
||||
ndefData = TryReadType4TagEnhanced(reader);
|
||||
break;
|
||||
|
||||
case NfcCardType.Type4:
|
||||
_logService?.Debug("=== Reading Type 4 Tag ===");
|
||||
ndefData = TryReadType4TagEnhanced(reader);
|
||||
if (ndefData != null && ndefData.Length > 0)
|
||||
{
|
||||
_logService?.Info("Successfully read NDEF data from Type 4 tag");
|
||||
return ndefData;
|
||||
}
|
||||
// Fallback to Type 2 if Type 4 fails
|
||||
_logService?.Debug("Type 4 read failed, trying Type 2 as fallback");
|
||||
ndefData = TryReadType2TagEnhanced(reader);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Try both methods if unknown
|
||||
_logService?.Debug("Unknown card type, trying all methods");
|
||||
ndefData = TryReadType4TagEnhanced(reader);
|
||||
if (ndefData == null || ndefData.Length == 0)
|
||||
{
|
||||
ndefData = TryReadType2TagEnhanced(reader);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Strategy 2: Try direct NDEF file read
|
||||
_logService?.Debug("=== Attempting direct NDEF file read ===");
|
||||
ndefData = TryReadNdefDirect(reader);
|
||||
if (ndefData != null && ndefData.Length > 0)
|
||||
{
|
||||
_logService?.Info("Successfully read NDEF data using direct method");
|
||||
return ndefData;
|
||||
}
|
||||
|
||||
// Strategy 3: Try reading raw tag memory (Type 2)
|
||||
_logService?.Debug("=== Attempting Type 2 Tag read ===");
|
||||
ndefData = TryReadType2Tag(reader);
|
||||
if (ndefData != null && ndefData.Length > 0)
|
||||
{
|
||||
_logService?.Info("Successfully read data using Type 2 method");
|
||||
_logService?.Info($"Successfully read {ndefData.Length} bytes of NDEF data");
|
||||
return ndefData;
|
||||
}
|
||||
|
||||
@@ -275,6 +414,297 @@ public class CardReaderService : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private byte[]? TryReadType4TagEnhanced(ICardReader reader)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Step 1: Select NDEF Tag Application (AID: D2760000850101)
|
||||
var selectNdef = new byte[] { 0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00 };
|
||||
var response = TransmitApdu(reader, selectNdef, "Select NDEF Application");
|
||||
|
||||
if (!IsSuccess(response))
|
||||
{
|
||||
_logService?.Debug("NDEF application not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 2: Select Capability Container (CC) file
|
||||
var selectCC = new byte[] { 0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03 };
|
||||
response = TransmitApdu(reader, selectCC, "Select CC File");
|
||||
|
||||
if (!IsSuccess(response))
|
||||
{
|
||||
_logService?.Debug("Failed to select CC file");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 3: Read CC to get NDEF file info
|
||||
var readCC = new byte[] { 0x00, 0xB0, 0x00, 0x00, 0x0F };
|
||||
response = TransmitApdu(reader, readCC, "Read CC");
|
||||
|
||||
if (!IsSuccess(response) || response?.Length < 17)
|
||||
{
|
||||
_logService?.Debug("Failed to read CC");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse CC to get max NDEF size
|
||||
var ccData = new byte[15];
|
||||
if (response != null)
|
||||
{
|
||||
Array.Copy(response, ccData, Math.Min(15, response.Length - 2));
|
||||
}
|
||||
|
||||
// CC format: 2 bytes length, 1 byte version, 2 bytes MLe, 2 bytes MLc, then TLV
|
||||
var maxNdefSize = (ccData[3] << 8) | ccData[4]; // MLe (Maximum Length for data read)
|
||||
var maxApduSize = (ccData[5] << 8) | ccData[6]; // MLc (Maximum Length for command)
|
||||
_logService?.Debug($"CC: Max NDEF size = {maxNdefSize}, Max APDU size = {maxApduSize}");
|
||||
|
||||
// Step 4: Select NDEF file
|
||||
var selectNdefFile = new byte[] { 0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04 };
|
||||
response = TransmitApdu(reader, selectNdefFile, "Select NDEF File");
|
||||
|
||||
if (!IsSuccess(response))
|
||||
{
|
||||
_logService?.Debug("Failed to select NDEF file");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 5: Read NDEF length (always 2 bytes for Type 4 tags per NFC Forum spec)
|
||||
var readLength = new byte[] { 0x00, 0xB0, 0x00, 0x00, 0x02 }; // Read 2 bytes
|
||||
response = TransmitApdu(reader, readLength, "Read NDEF Length");
|
||||
|
||||
if (response == null || response.Length < 4)
|
||||
{
|
||||
_logService?.Debug("Failed to read NDEF length");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Type 4 tags use 2-byte NLEN field (big-endian)
|
||||
int ndefLength = (response[0] << 8) | response[1];
|
||||
int dataOffset = 2; // NDEF message starts at byte 2
|
||||
|
||||
_logService?.Debug($"NDEF length: {ndefLength} bytes (0x{response[0]:X2}{response[1]:X2})");
|
||||
|
||||
if (ndefLength == 0)
|
||||
{
|
||||
_logService?.Warning("NDEF length is 0 - empty tag");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ndefLength > 65535)
|
||||
{
|
||||
_logService?.Warning($"NDEF length too large: {ndefLength}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 6: Read NDEF data in chunks
|
||||
var allData = new List<byte>();
|
||||
var offset = dataOffset;
|
||||
var maxReadSize = Math.Min(maxApduSize > 0 ? maxApduSize : 250, 250); // Use CC info or default to 250
|
||||
|
||||
while (allData.Count < ndefLength)
|
||||
{
|
||||
var remainingBytes = ndefLength - allData.Count;
|
||||
var readSize = Math.Min(remainingBytes, maxReadSize);
|
||||
|
||||
var readData = new byte[] {
|
||||
0x00, 0xB0,
|
||||
(byte)(offset >> 8), (byte)(offset & 0xFF),
|
||||
(byte)readSize
|
||||
};
|
||||
|
||||
response = TransmitApdu(reader, readData, $"Read NDEF chunk at offset {offset}");
|
||||
|
||||
if (!IsSuccess(response))
|
||||
{
|
||||
_logService?.Error($"Failed to read NDEF data at offset {offset}");
|
||||
break;
|
||||
}
|
||||
|
||||
var dataLength = response?.Length - 2 ?? 0;
|
||||
if (dataLength > 0 && response != null)
|
||||
{
|
||||
for (int i = 0; i < dataLength && allData.Count < ndefLength; i++)
|
||||
{
|
||||
allData.Add(response[i]);
|
||||
}
|
||||
offset += dataLength;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logService?.Warning("No data received in chunk read");
|
||||
break;
|
||||
}
|
||||
|
||||
_logService?.Debug($"Read {dataLength} bytes, total: {allData.Count}/{ndefLength}");
|
||||
}
|
||||
|
||||
if (allData.Count > 0)
|
||||
{
|
||||
_logService?.Info($"Successfully read {allData.Count} bytes from Type 4 tag");
|
||||
return allData.ToArray();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logService?.Debug($"Type 4 enhanced read exception: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[]? TryReadType2TagEnhanced(ICardReader reader)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logService?.Debug("Starting enhanced Type 2 tag read");
|
||||
|
||||
// Step 1: Read first 16 bytes (blocks 0-3) to identify tag
|
||||
var readHeader = new byte[] { 0x30, 0x00 }; // READ from block 0
|
||||
var response = TransmitApdu(reader, readHeader, "Read header blocks 0-3");
|
||||
|
||||
if (response == null || response.Length < 16)
|
||||
{
|
||||
_logService?.Debug("Failed to read header blocks");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 2: Check Capability Container (CC) at block 3 (bytes 12-15)
|
||||
// CC format: Magic number, Version, Memory size, Read/Write access
|
||||
if (response.Length >= 16)
|
||||
{
|
||||
var cc0 = response[12]; // Should be 0xE1 for NDEF
|
||||
var cc1 = response[13]; // Version
|
||||
var cc2 = response[14]; // Memory size
|
||||
var cc3 = response[15]; // Read/Write access
|
||||
|
||||
_logService?.Debug($"CC: {cc0:X2} {cc1:X2} {cc2:X2} {cc3:X2}");
|
||||
|
||||
if (cc0 != 0xE1)
|
||||
{
|
||||
_logService?.Debug("Not an NDEF formatted tag (CC0 != 0xE1)");
|
||||
// Continue anyway as some tags might still have NDEF data
|
||||
}
|
||||
|
||||
var memorySize = (cc2 & 0xFF) * 8;
|
||||
_logService?.Debug($"Tag memory size: {memorySize} bytes");
|
||||
}
|
||||
|
||||
// Step 3: Read NDEF data starting from block 4
|
||||
var allData = new List<byte>();
|
||||
byte currentBlock = 4;
|
||||
int tlvPosition = 0;
|
||||
bool ndefFound = false;
|
||||
int ndefLength = 0;
|
||||
|
||||
// Read up to 64 blocks (256 bytes) or until terminator
|
||||
while (currentBlock < 64)
|
||||
{
|
||||
var readBlock = new byte[] { 0x30, currentBlock };
|
||||
response = TransmitApdu(reader, readBlock, $"Read block {currentBlock}");
|
||||
|
||||
if (response == null || response.Length < 16)
|
||||
{
|
||||
_logService?.Debug($"Failed to read block {currentBlock}");
|
||||
break;
|
||||
}
|
||||
|
||||
// Process 4 blocks (16 bytes) at a time
|
||||
for (int i = 0; i < 16 && (currentBlock * 4 + i / 4) < 256; i++)
|
||||
{
|
||||
allData.Add(response[i]);
|
||||
|
||||
// Parse TLV structure to find NDEF message
|
||||
if (!ndefFound && allData.Count >= 2)
|
||||
{
|
||||
// Check for NDEF TLV (0x03)
|
||||
if (allData[tlvPosition] == 0x03)
|
||||
{
|
||||
ndefFound = true;
|
||||
_logService?.Debug($"Found NDEF TLV at position {tlvPosition}");
|
||||
|
||||
// Parse length
|
||||
if (allData.Count > tlvPosition + 1)
|
||||
{
|
||||
var lengthByte = allData[tlvPosition + 1];
|
||||
if (lengthByte == 0xFF && allData.Count > tlvPosition + 3)
|
||||
{
|
||||
// 3-byte length
|
||||
ndefLength = (allData[tlvPosition + 2] << 8) | allData[tlvPosition + 3];
|
||||
tlvPosition += 4; // Skip TLV header
|
||||
}
|
||||
else
|
||||
{
|
||||
// 1-byte length
|
||||
ndefLength = lengthByte;
|
||||
tlvPosition += 2; // Skip TLV header
|
||||
}
|
||||
_logService?.Debug($"NDEF length: {ndefLength} bytes");
|
||||
}
|
||||
}
|
||||
else if (allData[tlvPosition] == 0xFE)
|
||||
{
|
||||
// Terminator TLV
|
||||
_logService?.Debug("Found terminator TLV");
|
||||
break;
|
||||
}
|
||||
else if (allData[tlvPosition] == 0x00)
|
||||
{
|
||||
// NULL TLV, skip
|
||||
tlvPosition++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Other TLV, skip based on length
|
||||
if (allData.Count > tlvPosition + 1)
|
||||
{
|
||||
var len = allData[tlvPosition + 1];
|
||||
tlvPosition += 2 + len;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we've read enough NDEF data
|
||||
if (ndefFound && allData.Count >= tlvPosition + ndefLength)
|
||||
{
|
||||
_logService?.Debug("Read complete NDEF message");
|
||||
break;
|
||||
}
|
||||
|
||||
currentBlock += 4; // Move to next set of 4 blocks
|
||||
}
|
||||
|
||||
// Extract NDEF data if found
|
||||
if (ndefFound && ndefLength > 0 && allData.Count >= tlvPosition + ndefLength)
|
||||
{
|
||||
var ndefData = new byte[ndefLength];
|
||||
for (int i = 0; i < ndefLength; i++)
|
||||
{
|
||||
ndefData[i] = allData[tlvPosition + i];
|
||||
}
|
||||
_logService?.Info($"Successfully extracted {ndefLength} bytes of NDEF data from Type 2 tag");
|
||||
return ndefData;
|
||||
}
|
||||
|
||||
// If no NDEF TLV found but we have data, return raw data
|
||||
if (allData.Count > 0)
|
||||
{
|
||||
_logService?.Warning("No NDEF TLV found, returning raw data");
|
||||
return allData.ToArray();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logService?.Debug($"Type 2 enhanced read exception: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[]? TryReadType4Tag(ICardReader reader)
|
||||
{
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user