Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54cea349c0 | |||
| a1d03fec2a | |||
| 308966da36 | |||
| cff9bc4876 | |||
| f3323420a8 |
@@ -3,7 +3,7 @@
|
||||
<Product Id="*"
|
||||
Name="NFC Actions"
|
||||
Language="1033"
|
||||
Version="1.0.0.0"
|
||||
Version="1.0.3.0"
|
||||
Manufacturer="Dangerous Things"
|
||||
UpgradeCode="A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D">
|
||||
|
||||
@@ -20,6 +20,15 @@
|
||||
<Property Id="DISABLEADVTSHORTCUTS" Value="1" />
|
||||
<Property Id="MSIINSTALLPERUSER" Value="1" />
|
||||
|
||||
<!-- Launch application after installation -->
|
||||
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch NFC Actions" />
|
||||
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1" />
|
||||
<Property Id="WixShellExecTarget" Value="[#NfcActionsExe]" />
|
||||
<CustomAction Id="LaunchApplication"
|
||||
BinaryKey="WixCA"
|
||||
DllEntry="WixShellExec"
|
||||
Impersonate="yes" />
|
||||
|
||||
<Icon Id="ProductIcon" SourceFile="..\NfcActions\Resources\icon.ico" />
|
||||
<Property Id="ARPPRODUCTICON" Value="ProductIcon" />
|
||||
<Property Id="ARPHELPLINK" Value="https://dangerousthings.com" />
|
||||
@@ -27,10 +36,26 @@
|
||||
|
||||
<Feature Id="ProductFeature" Title="NFC Actions" Level="1">
|
||||
<ComponentGroupRef Id="ProductComponents" />
|
||||
<ComponentGroupRef Id="HarvestedFiles" />
|
||||
<ComponentRef Id="ApplicationShortcut" />
|
||||
<ComponentRef Id="AutoStartRegistry" />
|
||||
</Feature>
|
||||
|
||||
<UI>
|
||||
<!-- Use WixUI_Minimal without license page -->
|
||||
<UIRef Id="WixUI_InstallDir" />
|
||||
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
|
||||
|
||||
<!-- Skip the license page -->
|
||||
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="2">1</Publish>
|
||||
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">1</Publish>
|
||||
|
||||
<Publish Dialog="ExitDialog"
|
||||
Control="Finish"
|
||||
Event="DoAction"
|
||||
Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>
|
||||
</UI>
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="LocalAppDataFolder">
|
||||
<Directory Id="CompanyFolder" Name="DangerousThings">
|
||||
@@ -72,30 +97,21 @@
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<!-- Registry and cleanup component -->
|
||||
<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
|
||||
<Component Id="MainExecutable" Guid="D4E5F6A7-B8C9-4D8E-1F2A-3B4C5D6E7F8A">
|
||||
<File Id="NfcActionsExe"
|
||||
Source="..\NfcActions\bin\Release\net7.0-windows\win-x64\publish\NfcActions.exe" />
|
||||
<Component Id="RegistryEntries" Guid="D4E5F6A7-B8C9-4D8E-1F2A-3B4C5D6E7F8A">
|
||||
<RegistryValue Root="HKCU"
|
||||
Key="Software\DangerousThings\NfcActions"
|
||||
Name="MainExe"
|
||||
Name="Installed"
|
||||
Type="integer"
|
||||
Value="1"
|
||||
KeyPath="yes"/>
|
||||
<RemoveFolder Id="RemoveINSTALLFOLDER" Directory="INSTALLFOLDER" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveCompanyFolder" Directory="CompanyFolder" On="uninstall" />
|
||||
</Component>
|
||||
|
||||
<Component Id="ResourcesFolder" Guid="E5F6A7B8-C9D0-4E9F-2A3B-4C5D6E7F8A9B">
|
||||
<File Id="IconIco" Source="..\NfcActions\bin\Release\net7.0-windows\win-x64\publish\Resources\icon.ico" />
|
||||
<RegistryValue Root="HKCU"
|
||||
Key="Software\DangerousThings\NfcActions"
|
||||
Name="Resources"
|
||||
Type="integer"
|
||||
Value="1"
|
||||
KeyPath="yes"/>
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
|
||||
<!-- Harvested files will be included from HarvestedFiles.wxs -->
|
||||
|
||||
</Product>
|
||||
</Wix>
|
||||
|
||||
BIN
NfcActions-Setup-v1.0.2-NET8.msi
Normal file
BIN
NfcActions-Setup-v1.0.2-NET8.msi
Normal file
Binary file not shown.
BIN
NfcActions-Setup-v1.0.2.msi
Normal file
BIN
NfcActions-Setup-v1.0.2.msi
Normal file
Binary file not shown.
7
NfcActions/App.config
Normal file
7
NfcActions/App.config
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<!-- Force software rendering to prevent DirectWrite crashes -->
|
||||
<AppContextSwitchOverrides value="Switch.System.Windows.DoNotUsePresentationDpiCapabilityTier2OrGreater=true;Switch.System.Windows.DoNotScaleForDpiChanges=false" />
|
||||
</runtime>
|
||||
</configuration>
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
using System.Windows.Media;
|
||||
using NfcActions.Services;
|
||||
using NfcActions.ViewModels;
|
||||
using Application = System.Windows.Application;
|
||||
@@ -13,6 +15,7 @@ namespace NfcActions;
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
private static Mutex? _instanceMutex;
|
||||
private NotifyIcon? _notifyIcon;
|
||||
private Icon? _customIcon;
|
||||
private MainWindow? _mainWindow;
|
||||
@@ -21,9 +24,42 @@ public partial class App : Application
|
||||
private ActionService? _actionService;
|
||||
private LogService? _logService;
|
||||
private MainViewModel? _viewModel;
|
||||
private System.ComponentModel.CancelEventHandler? _windowClosingHandler;
|
||||
|
||||
public App()
|
||||
{
|
||||
// CRITICAL: Set software rendering mode BEFORE any WPF initialization
|
||||
// This must be done in the constructor before InitializeComponent()
|
||||
try
|
||||
{
|
||||
// Set environment variable as additional safeguard
|
||||
Environment.SetEnvironmentVariable("DOTNET_SYSTEM_WINDOWS_DONOTUSEPRESENTATIONDPICAPABILITYTIER2ORGREATER", "1");
|
||||
|
||||
// Force software rendering
|
||||
RenderOptions.ProcessRenderMode = System.Windows.Interop.RenderMode.SoftwareOnly;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If this fails, try to continue anyway
|
||||
}
|
||||
}
|
||||
|
||||
private void Application_Startup(object sender, StartupEventArgs e)
|
||||
{
|
||||
// Ensure only one instance runs at a time
|
||||
bool createdNew;
|
||||
_instanceMutex = new Mutex(true, "NfcActions_SingleInstance_Mutex", out createdNew);
|
||||
|
||||
if (!createdNew)
|
||||
{
|
||||
System.Windows.MessageBox.Show("NFC Actions is already running. Check the system tray.",
|
||||
"NFC Actions",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Information);
|
||||
Shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize services
|
||||
_logService = new LogService();
|
||||
_cardReaderService = new CardReaderService(_logService);
|
||||
@@ -86,16 +122,44 @@ public partial class App : Application
|
||||
|
||||
private void ShowMainWindow()
|
||||
{
|
||||
// If window was closed, recreate it
|
||||
if (_mainWindow == null || !_mainWindow.IsLoaded)
|
||||
{
|
||||
if (_viewModel == null) return;
|
||||
|
||||
_mainWindow = new MainWindow(_viewModel);
|
||||
|
||||
// Handle window closing - hide instead of close
|
||||
_windowClosingHandler = (s, e) =>
|
||||
{
|
||||
e.Cancel = true;
|
||||
if (_mainWindow != null)
|
||||
{
|
||||
_mainWindow.Hide();
|
||||
}
|
||||
};
|
||||
|
||||
_mainWindow.Closing += _windowClosingHandler;
|
||||
}
|
||||
|
||||
_mainWindow.Show();
|
||||
_mainWindow.WindowState = WindowState.Normal;
|
||||
_mainWindow.Activate();
|
||||
}
|
||||
}
|
||||
|
||||
private void ExitApplication()
|
||||
{
|
||||
// Properly close the main window if it exists
|
||||
if (_mainWindow != null)
|
||||
{
|
||||
// Remove the cancel handler so the window can actually close
|
||||
if (_windowClosingHandler != null)
|
||||
{
|
||||
_mainWindow.Closing -= _windowClosingHandler;
|
||||
}
|
||||
_mainWindow.Close();
|
||||
}
|
||||
|
||||
_notifyIcon?.Dispose();
|
||||
_customIcon?.Dispose();
|
||||
_cardReaderService?.Dispose();
|
||||
@@ -107,5 +171,7 @@ public partial class App : Application
|
||||
_notifyIcon?.Dispose();
|
||||
_customIcon?.Dispose();
|
||||
_cardReaderService?.Dispose();
|
||||
_instanceMutex?.ReleaseMutex();
|
||||
_instanceMutex?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,16 @@
|
||||
|
||||
<!-- Header -->
|
||||
<Grid Grid.Row="0" Margin="0,0,0,15">
|
||||
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Left">
|
||||
<TextBlock Text="NFC Actions Configuration"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left"/>
|
||||
FontWeight="Bold"/>
|
||||
<TextBlock x:Name="VersionText"
|
||||
Text="v1.0.3"
|
||||
FontSize="10"
|
||||
Foreground="Gray"
|
||||
Margin="0,2,0,0"/>
|
||||
</StackPanel>
|
||||
<Image Source="Resources/logo.png"
|
||||
Height="60"
|
||||
HorizontalAlignment="Right"
|
||||
@@ -99,10 +104,20 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ListBox Grid.Row="0"
|
||||
x:Name="LogListBox"
|
||||
ItemsSource="{Binding LogEntries}"
|
||||
Margin="0,0,0,5"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto">
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
SelectionMode="Extended">
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Copy Selected Lines" Click="CopySelectedLogs_Click"/>
|
||||
<MenuItem Header="Copy All" Click="CopyAllLogs_Click"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="Select All" Click="SelectAllLogs_Click"/>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding FormattedMessage}"
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
using System.ComponentModel;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using NfcActions.Services;
|
||||
using NfcActions.ViewModels;
|
||||
|
||||
namespace NfcActions;
|
||||
@@ -15,6 +19,13 @@ public partial class MainWindow : Window
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = viewModel;
|
||||
|
||||
// Set version number from assembly
|
||||
var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
|
||||
if (version != null)
|
||||
{
|
||||
VersionText.Text = $"v{version.Major}.{version.Minor}.{version.Build}";
|
||||
}
|
||||
}
|
||||
|
||||
private void Window_Closing(object? sender, CancelEventArgs e)
|
||||
@@ -39,4 +50,84 @@ public partial class MainWindow : Window
|
||||
// Silently fail if browser can't be opened
|
||||
}
|
||||
}
|
||||
|
||||
private void CopySelectedLogs_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var selectedItems = LogListBox.SelectedItems.Cast<LogEntry>().ToList();
|
||||
if (selectedItems.Count == 0)
|
||||
{
|
||||
MessageBox.Show("No log entries selected. Please select one or more lines first.",
|
||||
"No Selection",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var logText = new StringBuilder();
|
||||
foreach (var entry in selectedItems)
|
||||
{
|
||||
logText.AppendLine(entry.FormattedMessage);
|
||||
}
|
||||
|
||||
Clipboard.SetText(logText.ToString());
|
||||
|
||||
MessageBox.Show($"Copied {selectedItems.Count} log {(selectedItems.Count == 1 ? "entry" : "entries")} to clipboard.",
|
||||
"Copied",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Failed to copy to clipboard: {ex.Message}",
|
||||
"Error",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyAllLogs_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (DataContext is MainViewModel viewModel)
|
||||
{
|
||||
var allEntries = viewModel.LogEntries.ToList();
|
||||
if (allEntries.Count == 0)
|
||||
{
|
||||
MessageBox.Show("No log entries available.",
|
||||
"Empty Log",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var logText = new StringBuilder();
|
||||
foreach (var entry in allEntries)
|
||||
{
|
||||
logText.AppendLine(entry.FormattedMessage);
|
||||
}
|
||||
|
||||
Clipboard.SetText(logText.ToString());
|
||||
|
||||
MessageBox.Show($"Copied all {allEntries.Count} log entries to clipboard.",
|
||||
"Copied",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Information);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Failed to copy to clipboard: {ex.Message}",
|
||||
"Error",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectAllLogs_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
LogListBox.SelectAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,26 +2,28 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>Resources\icon.ico</ApplicationIcon>
|
||||
<Version>1.0.0</Version>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<Version>1.0.3</Version>
|
||||
<AssemblyVersion>1.0.3.0</AssemblyVersion>
|
||||
<FileVersion>1.0.3.0</FileVersion>
|
||||
<Company>Dangerous Things</Company>
|
||||
<Product>NFC Actions</Product>
|
||||
<Description>NFC card reader monitoring and action automation</Description>
|
||||
<Copyright>Copyright © 2025 Dangerous Things</Copyright>
|
||||
<RollForward>LatestMinor</RollForward>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<SelfContained>true</SelfContained>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
<EnableCompressionInSingleFile>false</EnableCompressionInSingleFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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);
|
||||
// Use appropriate strategy based on detected card type
|
||||
switch (cardType)
|
||||
{
|
||||
case NfcCardType.Type2:
|
||||
_logService?.Debug("=== Reading Type 2 Tag ===");
|
||||
ndefData = TryReadType2TagEnhanced(reader);
|
||||
if (ndefData != null && ndefData.Length > 0)
|
||||
{
|
||||
_logService?.Info("Successfully read NDEF data using Type 4 method");
|
||||
_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;
|
||||
|
||||
// Strategy 2: Try direct NDEF file read
|
||||
_logService?.Debug("=== Attempting direct NDEF file read ===");
|
||||
ndefData = TryReadNdefDirect(reader);
|
||||
case NfcCardType.Type4:
|
||||
_logService?.Debug("=== Reading Type 4 Tag ===");
|
||||
ndefData = TryReadType4TagEnhanced(reader);
|
||||
if (ndefData != null && ndefData.Length > 0)
|
||||
{
|
||||
_logService?.Info("Successfully read NDEF data using direct method");
|
||||
_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 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
|
||||
|
||||
74
RELEASE_NOTES.md
Normal file
74
RELEASE_NOTES.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# NFC Actions v1.0.0 - Initial Release
|
||||
|
||||
## Features
|
||||
|
||||
### Core Functionality
|
||||
- **System Tray Application** - Runs quietly in the background, accessible from the system tray
|
||||
- **Real-time NFC Monitoring** - Automatically detects and monitors all PC/SC compatible NFC readers
|
||||
- **Dynamic Reader Management** - Handles USB reader plug/unplug events automatically
|
||||
- **NDEF Payload Extraction** - Supports Type 2 and Type 4 NFC tags with automatic block size detection
|
||||
|
||||
### Actions
|
||||
- **Copy to Clipboard** - Copies NDEF payload data to clipboard for easy pasting
|
||||
- **Launch URLs** - Opens URI records in your default browser (URL records only)
|
||||
- **Keyboard Input** - Types NDEF content as keyboard input into active application
|
||||
|
||||
### User Interface
|
||||
- **Configuration Window** - Clean, simple interface for managing readers and actions
|
||||
- **Real-time Activity Log** - Color-coded logging (Debug, Info, Warning, Error) for visibility
|
||||
- **Dangerous Things Branding** - Custom icons and clickable logo
|
||||
|
||||
### Technical Features
|
||||
- **Settings Persistence** - Remembers your preferences between sessions
|
||||
- **Auto-start on Login** - Automatically starts when you log in to Windows
|
||||
- **File Logging** - Debug logs saved to application directory for troubleshooting
|
||||
- **Single-file Deployment** - No .NET runtime installation required
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download **NfcActions-Setup.msi** from the release assets
|
||||
2. Run the installer
|
||||
3. The application will:
|
||||
- Install to `%LOCALAPPDATA%\DangerousThings\NFC Actions`
|
||||
- Create a Start Menu shortcut
|
||||
- Configure automatic startup on login
|
||||
- Start running immediately in the system tray
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Operating System**: Windows 10 or Windows 11
|
||||
- **Hardware**: PC/SC compatible NFC reader (USB or built-in)
|
||||
- **Runtime**: None required (self-contained)
|
||||
|
||||
## Tested Readers
|
||||
|
||||
- Identiv uTrust 3700 F
|
||||
- HID OMNIKEY 5022 CL
|
||||
|
||||
## Usage
|
||||
|
||||
1. **First Launch**: Click the tray icon to open the configuration window
|
||||
2. **Enable/Disable Readers**: Check or uncheck readers in the "Active Readers" list
|
||||
3. **Configure Actions**: Select which actions to perform when a card is detected
|
||||
4. **Tap NFC Card**: Simply tap your NFC card to any enabled reader
|
||||
5. **Monitor Activity**: Watch the activity log for real-time feedback
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Only the first NDEF record is processed (multiple records not supported yet)
|
||||
- Only NDEF payload is used (not the full NDEF message structure)
|
||||
- URI detection is limited to standard URI record types and identifier codes
|
||||
|
||||
## Support
|
||||
|
||||
- **Website**: https://dangerousthings.com
|
||||
- **Repository**: https://git.dngr.us/DangerousThings/nfc-actions
|
||||
- **Issues**: Report bugs via the repository issue tracker
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2025 Dangerous Things
|
||||
|
||||
---
|
||||
|
||||
**Built with Claude Code** - https://claude.com/claude-code
|
||||
@@ -9,7 +9,7 @@ param(
|
||||
$ErrorActionPreference = "Stop"
|
||||
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$projectPath = Join-Path $scriptPath "NfcActions\NfcActions.csproj"
|
||||
$publishPath = Join-Path $scriptPath "NfcActions\bin\Release\net7.0-windows\win-x64\publish"
|
||||
$publishPath = Join-Path $scriptPath "NfcActions\bin\Release\net8.0-windows\win-x64\publish"
|
||||
$installerPath = Join-Path $scriptPath "Installer"
|
||||
|
||||
Write-Host "=== NFC Actions Release Build ===" -ForegroundColor Cyan
|
||||
@@ -70,8 +70,28 @@ if ($BuildInstaller) {
|
||||
|
||||
Push-Location $installerPath
|
||||
|
||||
# Run candle (compile)
|
||||
& candle.exe Product.wxs -out obj\Product.wixobj
|
||||
# Create obj and bin directories if they don't exist
|
||||
New-Item -ItemType Directory -Force -Path obj | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path bin | Out-Null
|
||||
|
||||
# Run heat to harvest all files from publish folder
|
||||
Write-Host " Harvesting files from publish folder..." -ForegroundColor Gray
|
||||
& heat.exe dir $publishPath -cg HarvestedFiles -gg -sfrag -srd -dr INSTALLFOLDER -var var.PublishDir -out obj\HarvestedFiles.wxs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Pop-Location
|
||||
Write-Host "Heat (file harvesting) failed!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Update the harvested file to set ID to NfcActions.exe
|
||||
Write-Host " Setting file ID for NfcActions.exe..." -ForegroundColor Gray
|
||||
$harvestedContent = Get-Content obj\HarvestedFiles.wxs -Raw
|
||||
$harvestedContent = $harvestedContent -replace '(<File Id=")([^"]+)(" KeyPath="yes" Source="\$\(var\.PublishDir\)\\NfcActions\.exe")', '$1NfcActionsExe$3'
|
||||
Set-Content obj\HarvestedFiles.wxs -Value $harvestedContent
|
||||
|
||||
# Run candle (compile) on both wxs files
|
||||
Write-Host " Compiling installer..." -ForegroundColor Gray
|
||||
& candle.exe Product.wxs obj\HarvestedFiles.wxs "-dPublishDir=$publishPath" -out obj\ -arch x64
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Pop-Location
|
||||
Write-Host "Candle (WiX compile) failed!" -ForegroundColor Red
|
||||
@@ -79,7 +99,8 @@ if ($BuildInstaller) {
|
||||
}
|
||||
|
||||
# Run light (link)
|
||||
& light.exe obj\Product.wixobj -out bin\NfcActions-Setup.msi -ext WixUIExtension
|
||||
Write-Host " Linking MSI package..." -ForegroundColor Gray
|
||||
& light.exe obj\Product.wixobj obj\HarvestedFiles.wixobj -out bin\NfcActions-Setup.msi -ext WixUIExtension -ext WixUtilExtension -sval
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Pop-Location
|
||||
Write-Host "Light (WiX link) failed!" -ForegroundColor Red
|
||||
|
||||
Reference in New Issue
Block a user