Compare commits

...

9 commits

4 changed files with 209 additions and 0 deletions

View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="PCSC" Version="6.1.3" />
<PackageReference Include="PCSC.Iso7816" Version="6.1.3" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>
</Project>

182
AfRApay.FTM/Program.cs Normal file
View file

@ -0,0 +1,182 @@
using System.CommandLine;
using PCSC;
using PCSC.Monitoring;
using PCSC.Iso7816;
const decimal DefaultAmount = 1.50M;
var rootCommand = new RootCommand("Fancy Test Machine for AfRApay");
var listReadersOption = new Option<bool>("--list-readers", "List card readers and exit");
rootCommand.Add(listReadersOption);
var webAddrOption = new Option<Uri>("--web-addr", "Base URL for AfRApay.Web");
webAddrOption.SetDefaultValue(new Uri("http://127.0.0.1:5296"));
rootCommand.Add(webAddrOption);
rootCommand.SetHandler((listReaders, webAddr) => {
using (var context = ContextFactory.Instance.Establish(SCardScope.System)) {
// Ignore Yubikeys.
var readerNames = context.GetReaders()
.Where((readerName) => !readerName.Contains("Yubico"))
.ToArray();
// We need at least one card reader or this won't work!
if (readerNames.Length == 0) {
Console.Error.WriteLine("Error: no card reader detected");
Environment.Exit(1);
}
// If --list-readers is passed, list readers and exit.
if (listReaders) {
Console.Error.WriteLine("----------- Connected Readers ----------");
foreach (var name in readerNames) {
Console.WriteLine(name);
}
Console.Error.WriteLine("----------------------------------------");
Environment.Exit(0);
}
using HttpClient httpClient = new();
httpClient.BaseAddress = webAddr;
Console.Error.WriteLine("----------------------------------------");
Console.Error.WriteLine("--- AfRApay FTM - Fancy Test Machine ---");
Console.Error.WriteLine("----------------------------------------");
Console.Error.WriteLine();
Console.Error.WriteLine("AfRApay.Web: {0}", httpClient.BaseAddress);
Console.Error.WriteLine();
Console.Error.WriteLine("Hotkeys (case insensitive):");
Console.Error.WriteLine(" [-] Debit (default)");
Console.Error.WriteLine(" [+] Credit");
Console.Error.WriteLine(" [=] Set amount (default: €1.50)");
Console.Error.WriteLine();
Console.Error.WriteLine(" [B] Balance query");
Console.Error.WriteLine(" [L] Link card (initiate from web UI)");
Console.Error.WriteLine(" [Esc] Cancel, reset state and amount");
Console.Error.WriteLine();
Console.Error.WriteLine("----------------------------------------");
// Listen for events on all connected readers.
using (var monitor = MonitorFactory.Instance.Create(SCardScope.System)) {
var state = TerminalState.Debit;
decimal amount = DefaultAmount;
monitor.Initialized += (_, args) => Console.WriteLine("[ Reader Initialized: {0} ]", args.ReaderName);
monitor.MonitorException += (_, args) => {
Console.Error.WriteLine("! ERROR: {0}", args);
Environment.Exit(1);
};
monitor.StatusChanged += (_, args) => Console.WriteLine("~ {0} -> {1}", args.LastState, args.NewState);
monitor.CardInserted += (_, args) => {
Console.WriteLine("> TAP: {0}", Convert.ToHexString(args.Atr));
var reader = new IsoReader(context, args.ReaderName, SCardShareMode.Shared, SCardProtocol.Any);
HandleTap(reader, httpClient, state, amount);
};
monitor.CardRemoved += (_, args) => {
Console.WriteLine("< OFF");
Console.WriteLine(); // Write a blank line between card taps for readability.
};
Console.WriteLine("[ Starting... ]");
monitor.Start(readerNames);
while (true) {
var key = Console.ReadKey();
var dontPrint = false;
switch (key.Key) {
case ConsoleKey.Subtract:
case ConsoleKey.OemMinus:
state = TerminalState.Debit;
break;
case ConsoleKey.Add:
case ConsoleKey.OemPlus:
state = TerminalState.Credit;
break;
case (ConsoleKey)0 when key.KeyChar == '=':
Console.Error.Write("\b => ENTER AMOUNT: ");
amount = Math.Abs(Decimal.Parse(Console.ReadLine() ?? "1.50".Trim()));
break;
case ConsoleKey.L:
state = TerminalState.Link;
break;
case ConsoleKey.B:
state = TerminalState.Balance;
break;
case ConsoleKey.Escape:
state = TerminalState.Debit;
amount = DefaultAmount;
break;
default:
Console.Error.Write("\b");
dontPrint = true;
break;
};
if (dontPrint) {
// Invalid input, just ignore it.
} else if (state == TerminalState.Debit || state == TerminalState.Credit) {
Console.Error.WriteLine("\b => {0}: €{1}", state, amount);
} else {
Console.Error.WriteLine("\b => {0}", state);
}
}
}
}
}, listReadersOption, webAddrOption);
return await rootCommand.InvokeAsync(args);
// Queries a card for data when one is tapped.
static async void HandleTap(IsoReader reader, HttpClient httpClient, TerminalState state, decimal amount) {
// Send a PCSC pseudo-APDU to query the ISO 14443 UID.
var uidRsp = reader.Transmit(new CommandApdu(IsoCase.Case2Short, SCardProtocol.Any) {
CLA = 0xFF,
Instruction = InstructionCode.GetData,
P1 = 0x00,
P2 = 0x00,
});
if (!IsSucc(uidRsp)) {
Console.Error.WriteLine("--> Card Error: SW1={0} SW2={1}", (SW1Code)uidRsp.SW1, uidRsp.SW2);
return;
}
var uid = uidRsp.GetData();
Console.WriteLine(" UID: {0}", Convert.ToHexString(uid));
// Query the backend, which endpoint depending on terminal state.
switch (state) {
case TerminalState.Debit:
case TerminalState.Credit:
var finalAmount = Math.Abs(amount) * (state == TerminalState.Debit ? 1 : -1);
await CallGet(httpClient, String.Format("/api/card/transaction?card={0}&amount={1}", Convert.ToHexString(uid), finalAmount));
break;
case TerminalState.Link:
await CallGet(httpClient, String.Format("/api/card/link?card={0}", Convert.ToHexString(uid)));
break;
case TerminalState.Balance:
await CallGet(httpClient, String.Format("/api/card/balance?card={0}", Convert.ToHexString(uid)));
break;
default:
Console.Error.WriteLine("UNKNOWN TERMINAL STATE: {0}", state);
break;
}
}
// Was the command successful?
static bool IsSucc(Response rsp) {
return rsp.SW1 == (byte)SW1Code.Normal && rsp.SW2 == 0x00;
}
static async Task<string> CallGet(HttpClient client, string path) {
Console.WriteLine(" -> GET {0}", path);
var rsp = await client.GetStringAsync(path);
Console.WriteLine(" <- {0}", rsp);
return rsp;
}
// Terminal State.
enum TerminalState {
Debit,
Credit,
Link,
Balance,
};

View file

@ -1,7 +1,10 @@

Microsoft Visual Studio Solution File, Format Version 12.00
#
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AfRApay.Web", "AfRApay.Web\AfRApay.Web.csproj", "{7187F28E-58C5-44BC-BDBA-17C387EA7AF3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AfRApay.FTM", "AfRApay.FTM\AfRApay.FTM.csproj", "{019D3978-5C9A-40C6-BFA0-0742E9F561E7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -12,5 +15,9 @@ Global
{7187F28E-58C5-44BC-BDBA-17C387EA7AF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7187F28E-58C5-44BC-BDBA-17C387EA7AF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7187F28E-58C5-44BC-BDBA-17C387EA7AF3}.Release|Any CPU.Build.0 = Release|Any CPU
{019D3978-5C9A-40C6-BFA0-0742E9F561E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{019D3978-5C9A-40C6-BFA0-0742E9F561E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{019D3978-5C9A-40C6-BFA0-0742E9F561E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{019D3978-5C9A-40C6-BFA0-0742E9F561E7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -3,8 +3,12 @@ pkgs.mkShell {
name = "afrapay";
packages = with pkgs; [
dotnet-sdk_7
pcsclite
# ESP32 tooling (for MateCard)
platformio
];
# AfRApay.FTM tries to dlopen the pcsclite library at runtime.
LD_LIBRARY_PATH = [ "${pkgs.pcsclite.out}/lib" ];
}