Initial commit

This commit is contained in:
2026-05-17 01:52:51 +08:00
commit 2dd378b17c
43 changed files with 3146 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using NfcAime.Dll.PN532;
using static NfcAime.Dll.MiFareHandle;
namespace NfcAime.Dll;
public class AimeReader
{
private string Port = "COM15";
private int Baud = 115200;
private Pn532Session session = null;
public enum CardKind { Felica, MifareClassic, Null}
private sealed record FlowResult(CardKind CardKind, byte[] CardId, string AccessCode);
private sealed record CardTarget(CardKind Kind, byte Tg, byte[] CardId);
// 兼容低版本 .NET实现 ToHexString 和 FromHexString
public static string ToHexString(byte[] bytes)
{
return BitConverter.ToString(bytes).Replace("-", "");
}
public static byte[] FromHexString(string hex)
{
if (hex.Length % 2 != 0)
throw new ArgumentException("Invalid hex string length.");
var bytes = new byte[hex.Length / 2];
for (int i = 0; i < bytes.Length; i++)
bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
return bytes;
}
public AimeReader(string port, int baud)
{
Port = port;
Baud = baud;
TimeSpan timeout = TimeSpan.FromMilliseconds(100);
using var transport = new SerialFrameTransport(Port, Baud, timeout);
session = new Pn532Session(transport, timeout, 0);
}
public (CardKind CardKind, byte[] IDm, string AccessCode) ReadCard()
{
session.Open();
try
{
var result = RunPn532Flow(session);
return (result.CardKind , result.CardId, result.AccessCode);
}
finally { session.Close(); }
}
public void CloseReader()
{
session.Close();
}
private FlowResult RunPn532Flow(Pn532Session session)
{
ExpectPn532ResponseCode(session.SendCommand(new byte[] { 0x02 }), expectedResponseCode: 0x03);
ExpectPn532StatusOk(session.SendCommand(new byte[] { 0x14, 0x01, 0x14, 0x01 }), expectedResponseCode: 0x15);
ExpectPn532StatusOk(session.SendCommand(new byte[] { 0x32, 0x01, 0x03 }), expectedResponseCode: 0x33);
var target = WaitForCard(session);
Thread.Sleep(100);
if (target.Kind == CardKind.Null)
{
return new FlowResult(target.Kind, target.CardId, "");
}
if (target.Kind == CardKind.Felica)
{
Console.WriteLine($"Card detected! IDm: {ToHexString(target.CardId)}");
var readCmd = FelicaCommandBuilder.BuildReadWithoutEncryptionCommand(target.CardId);
var readResponse = SendInDataExchange(session, target.Tg, readCmd, TimeSpan.FromSeconds(5));
var spad0 = FelicaResponseParser.ParseSpad0(readResponse);
var decryptor = new FeliCaDecryptor();
var decrypted = decryptor.Decrypt(spad0);
var accessCode = AccessCodeFormatter.ToAccessCodeString(decrypted);
return new FlowResult(target.Kind, target.CardId, accessCode);
}
Console.WriteLine($"Card detected! TypeA UID: {ToHexString(target.CardId)}");
var m1AccessCode = TryReadMifareClassicAccessCode(session, target.Tg, target.CardId);
if (m1AccessCode is null)
{
throw new InvalidOperationException("Failed to read Mifare Classic AccessCode: no key matched sector 0.");
}
return new FlowResult(target.Kind, target.CardId, m1AccessCode);
}
private byte[] SendInDataExchange(Pn532Session session, byte tg, ReadOnlySpan<byte> payloadToTarget, TimeSpan? timeout = null)
{
var pn532Payload = new byte[2 + payloadToTarget.Length];
pn532Payload[0] = 0x40; pn532Payload[1] = tg;
payloadToTarget.CopyTo(pn532Payload.AsSpan(2));
var response = ExpectPn532ResponseCode(session.SendCommand(pn532Payload, responseTimeout: timeout ?? TimeSpan.FromSeconds(4)), expectedResponseCode: 0x41);
if (response.Payload.Length < 2 || response.Payload[1] != 0x00)
throw new InvalidOperationException($"InDataExchange failed: 0x{(response.Payload.Length > 1 ? response.Payload[1] : 0):X2}");
// 替换 System.Range 语法
return response.Payload.Length == 2 ? Array.Empty<byte>() : response.Payload.Skip(2).ToArray();
}
private string? TryReadMifareClassicAccessCode(Pn532Session session, byte tg, ReadOnlySpan<byte> uid)
{
const byte blockNumber = 2; // sector 0 block 2
var uid4 = new byte[4];
Array.Copy(uid.ToArray(), 0, uid4, 0, 4);
foreach (var keyHex in MifareClassicKeys)
{
var key = FromHexString(keyHex);
if (TryMifareAuthenticate(session, tg, blockNumber, keyTypeA: true, key, uid4) ||
TryMifareAuthenticate(session, tg, blockNumber, keyTypeA: false, key, uid4))
{
var block = ReadMifareBlock(session, tg, blockNumber);
var hex = ToHexString(block);
return hex.Length <= 20 ? hex : hex.Substring(hex.Length - 20, 20);
}
}
return null;
}
public static Pn532FrameParseResult ExpectPn532ResponseCode(Pn532FrameParseResult response, byte expectedResponseCode)
{
if (response.Kind != Pn532FrameKind.Data) throw new InvalidOperationException($"PN532 error: {response.Kind} ({response.Error}).");
if (response.Payload.Length < 1 || response.Payload[0] != expectedResponseCode)
throw new InvalidOperationException($"Expected 0x{expectedResponseCode:X2}, got 0x{(response.Payload.Length > 0 ? response.Payload[0] : 0):X2}");
return response;
}
private void ExpectPn532StatusOk(Pn532FrameParseResult response, byte expectedResponseCode)
{
response = ExpectPn532ResponseCode(response, expectedResponseCode);
if (response.Payload.Length >= 2 && response.Payload[1] != 0x00) throw new InvalidOperationException($"Status fail: 0x{response.Payload[1]:X2}");
}
private CardTarget WaitForCard(Pn532Session session)
{
var f212 = session.SendCommand(new byte[] { 0x4A, 0x01, 0x01, 0x00, 0xFF, 0xFF, 0x01, 0x00 }, TimeSpan.FromMilliseconds(500));
if (ExtractFeliCaTarget(f212, out var target)) return target;
var a106 = session.SendCommand(new byte[] { 0x4A, 0x01, 0x00 }, TimeSpan.FromMilliseconds(600));
if (ExtractTypeATarget(a106, out target)) return target;
return new CardTarget(CardKind.Null, 0, [0]);
}
private bool ExtractFeliCaTarget(Pn532FrameParseResult result, out CardTarget target)
{
target = null;
if (result.Kind == Pn532FrameKind.Data && result.Payload.Length >= 15 && result.Payload[0] == 0x4B && result.Payload[1] > 0)
{
var tg = result.Payload[2];
var idm = result.Payload.AsSpan(5, 8).ToArray();
target = new CardTarget(CardKind.Felica, tg, idm);
return true;
}
return false;
}
private bool ExtractTypeATarget(Pn532FrameParseResult result, out CardTarget target)
{
target = null;
if (result.Kind == Pn532FrameKind.Data && result.Payload.Length >= 8 && result.Payload[0] == 0x4B && result.Payload[1] > 0)
{
var tg = result.Payload[2];
var uidLen = result.Payload[6];
var uidOffset = 7;
if (uidLen > 0 && result.Payload.Length >= uidOffset + uidLen)
{
var uid = result.Payload.AsSpan(uidOffset, uidLen).ToArray();
target = new CardTarget(CardKind.MifareClassic, tg, uid);
return true;
}
}
return false;
}
}