Initial commit: NFC Actions Windows tray application

Implement PC/SC-based NFC card reader monitoring application with WPF UI. Features include:
- System tray integration with single-click to open main window
- Dynamic reader detection and management with enable/disable per reader
- NDEF payload extraction supporting Type 2 and Type 4 tags
- Auto-detection of block sizes (4-byte vs 16-byte) for different reader types
- Configurable actions: copy to clipboard, launch URLs, keyboard input simulation
- URI record type detection - only launches browser for actual URI records
- Real-time activity logging with color-coded levels (Debug, Info, Warning, Error)
- File-based debug logging for troubleshooting
- Settings persistence between sessions
- Dangerous Things branding with custom icons and clickable logo

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-06 22:56:11 -08:00
commit 5b6a7f0fa5
20 changed files with 1981 additions and 0 deletions

View File

@@ -0,0 +1,311 @@
using System;
using System.Text;
namespace NfcActions.Services;
public class NdefRecord
{
public string Payload { get; set; } = string.Empty;
public bool IsUri { get; set; }
}
public static class NdefParser
{
/// <summary>
/// Extracts the first NDEF payload from raw card data.
/// Returns the payload as a string, or null if no valid NDEF message found.
/// </summary>
public static string? ExtractFirstPayload(byte[] data)
{
var record = ExtractFirstRecord(data);
return record?.Payload;
}
/// <summary>
/// Extracts the first NDEF record from raw card data with type information.
/// Returns an NdefRecord object, or null if no valid NDEF message found.
/// </summary>
public static NdefRecord? ExtractFirstRecord(byte[] data)
{
if (data == null || data.Length < 3)
return null;
try
{
int position = 0;
// Look for NDEF TLV (Type-Length-Value) structure
// We're looking for TLV type 0x03 which indicates NDEF Message
while (position < data.Length - 2)
{
byte tlvType = data[position];
if (tlvType == 0x00) // NULL TLV, skip
{
position++;
continue;
}
if (tlvType == 0xFE) // Terminator TLV
{
break;
}
position++; // Move to length byte
if (position >= data.Length)
break;
int length;
if (data[position] == 0xFF) // 3-byte length format
{
if (position + 2 >= data.Length)
break;
length = (data[position + 1] << 8) | data[position + 2];
position += 3;
}
else
{
length = data[position];
position++;
}
if (tlvType == 0x03) // NDEF Message TLV
{
if (position + length > data.Length)
break;
// Parse NDEF message
var record = ParseNdefMessage(data, position, length);
if (record != null)
return record;
}
position += length;
}
// If we didn't find TLV structure, try to parse as raw NDEF message
return ParseNdefMessage(data, 0, data.Length);
}
catch
{
return null;
}
}
private static NdefRecord? ParseNdefMessage(byte[] data, int offset, int length)
{
if (offset + length > data.Length || length < 3)
return null;
int position = offset;
int endPosition = offset + length;
while (position < endPosition)
{
if (position >= data.Length)
break;
byte header = data[position];
position++;
bool mb = (header & 0x80) != 0; // Message Begin
bool me = (header & 0x40) != 0; // Message End
bool cf = (header & 0x20) != 0; // Chunk Flag
bool sr = (header & 0x10) != 0; // Short Record
bool il = (header & 0x08) != 0; // ID Length present
byte tnf = (byte)(header & 0x07); // Type Name Format
if (position >= endPosition)
break;
int typeLength = data[position];
position++;
int typePosition = 0; // Track where type field starts
if (position >= endPosition)
break;
int payloadLength;
if (sr) // Short record - 1 byte payload length
{
payloadLength = data[position];
position++;
}
else // Normal record - 4 byte payload length
{
if (position + 3 >= endPosition)
break;
payloadLength = (data[position] << 24) |
(data[position + 1] << 16) |
(data[position + 2] << 8) |
data[position + 3];
position += 4;
}
int idLength = 0;
if (il)
{
if (position >= endPosition)
break;
idLength = data[position];
position++;
}
// Remember type position before skipping
typePosition = position;
// Read type field to check if it's a URI record
byte[]? typeField = null;
if (typeLength > 0 && position + typeLength <= endPosition)
{
typeField = new byte[typeLength];
Array.Copy(data, position, typeField, 0, typeLength);
}
// Skip type
position += typeLength;
// Skip ID
position += idLength;
if (position + payloadLength > endPosition)
break;
// Extract payload
if (payloadLength > 0)
{
byte[] payload = new byte[payloadLength];
Array.Copy(data, position, payload, 0, payloadLength);
// Determine if this is a URI record
bool isUri = IsUriRecord(tnf, typeField, payload);
// Decode payload
string payloadText = DecodeTextPayload(payload);
return new NdefRecord
{
Payload = payloadText,
IsUri = isUri
};
}
position += payloadLength;
// If this was the first record, return what we found (or null)
break;
}
return null;
}
private static bool IsUriRecord(byte tnf, byte[]? typeField, byte[] payload)
{
// TNF Well-Known (0x01) with type "U" is a URI record
if (tnf == 0x01 && typeField != null && typeField.Length == 1 && typeField[0] == 0x55) // 'U'
{
return true;
}
// TNF Absolute URI (0x03)
if (tnf == 0x03)
{
return true;
}
// Check if payload starts with URI identifier code (0x00-0x23)
if (payload.Length > 0 && payload[0] <= 0x23)
{
string prefix = GetUriPrefix(payload[0]);
// If we have a recognized URI prefix, it's likely a URI
if (!string.IsNullOrEmpty(prefix) || payload[0] == 0x00)
{
return true;
}
}
return false;
}
private static string DecodeTextPayload(byte[] payload)
{
if (payload.Length == 0)
return string.Empty;
// Check if first byte indicates URI identifier code
if (payload[0] <= 0x23) // URI identifier codes range from 0x00 to 0x23
{
string prefix = GetUriPrefix(payload[0]);
string uri = Encoding.UTF8.GetString(payload, 1, payload.Length - 1);
return prefix + uri;
}
// Check if it's a text record (first byte is status byte)
if (payload.Length > 1)
{
byte statusByte = payload[0];
bool isUtf16 = (statusByte & 0x80) != 0;
int languageCodeLength = statusByte & 0x3F;
if (languageCodeLength < payload.Length)
{
int textStart = 1 + languageCodeLength;
if (textStart < payload.Length)
{
var encoding = isUtf16 ? Encoding.Unicode : Encoding.UTF8;
return encoding.GetString(payload, textStart, payload.Length - textStart);
}
}
}
// Default: try UTF8 decoding of entire payload
return Encoding.UTF8.GetString(payload);
}
private static string GetUriPrefix(byte code)
{
return code switch
{
0x00 => "",
0x01 => "http://www.",
0x02 => "https://www.",
0x03 => "http://",
0x04 => "https://",
0x05 => "tel:",
0x06 => "mailto:",
0x07 => "ftp://anonymous:anonymous@",
0x08 => "ftp://ftp.",
0x09 => "ftps://",
0x0A => "sftp://",
0x0B => "smb://",
0x0C => "nfs://",
0x0D => "ftp://",
0x0E => "dav://",
0x0F => "news:",
0x10 => "telnet://",
0x11 => "imap:",
0x12 => "rtsp://",
0x13 => "urn:",
0x14 => "pop:",
0x15 => "sip:",
0x16 => "sips:",
0x17 => "tftp:",
0x18 => "btspp://",
0x19 => "btl2cap://",
0x1A => "btgoep://",
0x1B => "tcpobex://",
0x1C => "irdaobex://",
0x1D => "file://",
0x1E => "urn:epc:id:",
0x1F => "urn:epc:tag:",
0x20 => "urn:epc:pat:",
0x21 => "urn:epc:raw:",
0x22 => "urn:epc:",
0x23 => "urn:nfc:",
_ => ""
};
}
}