diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0626272 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode + +# Rider +.idea + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +msbuild.log +msbuild.err +msbuild.wrn + +# Visual Studio 2015 +.vs/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e035aa6 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# ThaiNationalIDCard.NET + +ThaiNationalIDCard.NET is wrapper classes for .NET based on the [PCSC](https://github.com/danm-de/pcsc-sharp) that provides access to the Thai National ID Card (Smart Card). The wrapper is written with .NET Standard version that are available on all .NET implementations. + +# Example +* Console Application +* Web API + +# Credits +* [ThaiNationalIDCard](https://github.com/chakphanu/ThaiNationalIDCard) +* [PC/SC wrapper classes for .NET](https://github.com/danm-de/pcsc-sharp) \ No newline at end of file diff --git a/ThaiNationalIDCard.NET.Example.ConsoleApp/Program.cs b/ThaiNationalIDCard.NET.Example.ConsoleApp/Program.cs new file mode 100644 index 0000000..23bc7ea --- /dev/null +++ b/ThaiNationalIDCard.NET.Example.ConsoleApp/Program.cs @@ -0,0 +1,34 @@ +using System; +using ThaiNationalIDCard.NET.Models; + +namespace ThaiNationalIDCard.NET.Example.ConsoleApp +{ + class Program + { + static void Main(string[] args) + { + try + { + ThaiNationalIDCardReader cardReader = new ThaiNationalIDCardReader(); + PersonalPhoto personalPhoto = cardReader.GetPersonalPhoto(); + + Console.WriteLine($"CitizenID: {personalPhoto.CitizenID}"); + Console.WriteLine($"ThaiPersonalInfo: {personalPhoto.ThaiPersonalInfo}"); + Console.WriteLine($"EnglishPersonalInfo: {personalPhoto.EnglishPersonalInfo}"); + Console.WriteLine($"Sex: {personalPhoto.Sex}"); + Console.WriteLine($"AddressInfo: {personalPhoto.AddressInfo}"); + Console.WriteLine($"IssueDate: {personalPhoto.IssueDate}"); + Console.WriteLine($"ExpireDate: {personalPhoto.ExpireDate}"); + Console.WriteLine($"Issuer: {personalPhoto.Issuer}"); + Console.WriteLine($"Photo: {personalPhoto.Photo}"); + } + catch (Exception e) + { + Console.WriteLine(e); + } + + Console.WriteLine("Please any key to exit..."); + Console.ReadKey(true); + } + } +} diff --git a/ThaiNationalIDCard.NET.Example.ConsoleApp/ThaiNationalIDCard.NET.Example.ConsoleApp.csproj b/ThaiNationalIDCard.NET.Example.ConsoleApp/ThaiNationalIDCard.NET.Example.ConsoleApp.csproj new file mode 100644 index 0000000..64c3385 --- /dev/null +++ b/ThaiNationalIDCard.NET.Example.ConsoleApp/ThaiNationalIDCard.NET.Example.ConsoleApp.csproj @@ -0,0 +1,12 @@ + + + + Exe + netcoreapp3.1 + + + + + + + diff --git a/ThaiNationalIDCard.NET.Example.WebApi/Controllers/ReadersController.cs b/ThaiNationalIDCard.NET.Example.WebApi/Controllers/ReadersController.cs new file mode 100644 index 0000000..921814b --- /dev/null +++ b/ThaiNationalIDCard.NET.Example.WebApi/Controllers/ReadersController.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System; +using ThaiNationalIDCard.NET.Models; + +namespace ThaiNationalIDCard.NET.Example.WebApi.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ReadersController : ControllerBase + { + private readonly ThaiNationalIDCardReader cardReader; + + public ReadersController(ThaiNationalIDCardReader cardReader) + { + this.cardReader = cardReader; + } + + [HttpGet("Read")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PersonalPhoto))] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public IActionResult Read() + { + try + { + PersonalPhoto personalPhoto = cardReader.GetPersonalPhoto(); + + return Ok(personalPhoto); + } + catch (Exception e) + { + return BadRequest(e); + } + } + } +} \ No newline at end of file diff --git a/ThaiNationalIDCard.NET.Example.WebApi/Program.cs b/ThaiNationalIDCard.NET.Example.WebApi/Program.cs new file mode 100644 index 0000000..b491573 --- /dev/null +++ b/ThaiNationalIDCard.NET.Example.WebApi/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace ThaiNationalIDCard.NET.Example.WebApi +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/ThaiNationalIDCard.NET.Example.WebApi/Properties/launchSettings.json b/ThaiNationalIDCard.NET.Example.WebApi/Properties/launchSettings.json new file mode 100644 index 0000000..080bb32 --- /dev/null +++ b/ThaiNationalIDCard.NET.Example.WebApi/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55527", + "sslPort": 0 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/readers/read", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "ThaiNationalIDCard.Example.WebApi": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/ThaiNationalIDCard.NET.Example.WebApi/Startup.cs b/ThaiNationalIDCard.NET.Example.WebApi/Startup.cs new file mode 100644 index 0000000..d8e1c7b --- /dev/null +++ b/ThaiNationalIDCard.NET.Example.WebApi/Startup.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace ThaiNationalIDCard.NET.Example.WebApi +{ + public class Startup + { + public IConfiguration Configuration { get; } + + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + + services.AddScoped(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/ThaiNationalIDCard.NET.Example.WebApi/ThaiNationalIDCard.NET.Example.WebApi.csproj b/ThaiNationalIDCard.NET.Example.WebApi/ThaiNationalIDCard.NET.Example.WebApi.csproj new file mode 100644 index 0000000..3a1e758 --- /dev/null +++ b/ThaiNationalIDCard.NET.Example.WebApi/ThaiNationalIDCard.NET.Example.WebApi.csproj @@ -0,0 +1,18 @@ + + + + netcoreapp3.1 + Pichid Detson + + + + + + + + + + + + + diff --git a/ThaiNationalIDCard.NET.Example.WebApi/appsettings.Development.json b/ThaiNationalIDCard.NET.Example.WebApi/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/ThaiNationalIDCard.NET.Example.WebApi/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/ThaiNationalIDCard.NET.Example.WebApi/appsettings.json b/ThaiNationalIDCard.NET.Example.WebApi/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/ThaiNationalIDCard.NET.Example.WebApi/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/ThaiNationalIDCard.NET.sln b/ThaiNationalIDCard.NET.sln new file mode 100644 index 0000000..2f4d4bf --- /dev/null +++ b/ThaiNationalIDCard.NET.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29613.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThaiNationalIDCard.NET", "ThaiNationalIDCard.NET\ThaiNationalIDCard.NET.csproj", "{754802D9-509B-4FE1-B9BB-8F0838B07AA7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThaiNationalIDCard.NET.Example.ConsoleApp", "ThaiNationalIDCard.NET.Example.ConsoleApp\ThaiNationalIDCard.NET.Example.ConsoleApp.csproj", "{94485686-CAF7-43FB-B425-DD9F13EFAFF5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThaiNationalIDCard.NET.Example.WebApi", "ThaiNationalIDCard.NET.Example.WebApi\ThaiNationalIDCard.NET.Example.WebApi.csproj", "{EC52B1CF-6467-4506-A7BC-CAD49E9F202F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {754802D9-509B-4FE1-B9BB-8F0838B07AA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {754802D9-509B-4FE1-B9BB-8F0838B07AA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {754802D9-509B-4FE1-B9BB-8F0838B07AA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {754802D9-509B-4FE1-B9BB-8F0838B07AA7}.Release|Any CPU.Build.0 = Release|Any CPU + {94485686-CAF7-43FB-B425-DD9F13EFAFF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94485686-CAF7-43FB-B425-DD9F13EFAFF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94485686-CAF7-43FB-B425-DD9F13EFAFF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94485686-CAF7-43FB-B425-DD9F13EFAFF5}.Release|Any CPU.Build.0 = Release|Any CPU + {EC52B1CF-6467-4506-A7BC-CAD49E9F202F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC52B1CF-6467-4506-A7BC-CAD49E9F202F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC52B1CF-6467-4506-A7BC-CAD49E9F202F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC52B1CF-6467-4506-A7BC-CAD49E9F202F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {61BA88EB-152B-47AA-970F-5E6FDF7BA682} + EndGlobalSection +EndGlobal diff --git a/ThaiNationalIDCard.NET/Interfaces/IThaiNationalIDCardAPDUCommand.cs b/ThaiNationalIDCard.NET/Interfaces/IThaiNationalIDCardAPDUCommand.cs new file mode 100644 index 0000000..5c9171f --- /dev/null +++ b/ThaiNationalIDCard.NET/Interfaces/IThaiNationalIDCardAPDUCommand.cs @@ -0,0 +1,15 @@ +namespace ThaiNationalIDCard.NET.Interfaces +{ + public interface IThaiNationalIDCardAPDUCommand + { + byte[] GetResponse(); + byte[] Select(byte[] command); + byte[] MinistryOfInteriorAppletCommand { get; } + byte[] CitizenIDCommand { get; } + byte[] PersonalInfoCommand { get; } + byte[] AddressInfoCommand { get; } + byte[] CardIssueExpireCommand { get; } + byte[] CardIssuerCommand { get; } + byte[][] PhotoCommand { get; } + } +} diff --git a/ThaiNationalIDCard.NET/Models/AddressInfo.cs b/ThaiNationalIDCard.NET/Models/AddressInfo.cs new file mode 100644 index 0000000..6327504 --- /dev/null +++ b/ThaiNationalIDCard.NET/Models/AddressInfo.cs @@ -0,0 +1,51 @@ +namespace ThaiNationalIDCard.NET.Models +{ + public class AddressInfo + { + public string HouseNo { get; set; } + public string VillageNo { get; set; } + public string Lane { get; set; } + public string Road { get; set; } + public string SubDistrict { get; set; } + public string District { get; set; } + public string Province { get; set; } + + public AddressInfo(string addressInfo) + { + string[] infos = addressInfo.Split('#'); + + HouseNo = infos[0].Trim(); + VillageNo = infos[1].Trim(); + Lane = infos[2].Trim(); + Road = infos[3].Trim(); + SubDistrict = infos[5].Trim(); + District = infos[6].Trim(); + Province = infos[7].Trim(); + } + + public override string ToString() + { + string address = HouseNo; + + if (!string.IsNullOrEmpty(VillageNo)) + address += string.Format($" ม.{VillageNo}"); + + if (!string.IsNullOrEmpty(Lane)) + address += string.Format($" ซ.{Lane}"); + + if (!string.IsNullOrEmpty(Road)) + address += string.Format($" ถ.{Road}"); + + if (!string.IsNullOrEmpty(SubDistrict)) + address += string.Format($" ต.{SubDistrict}"); + + if (!string.IsNullOrEmpty(District)) + address += string.Format($" อ.{District}"); + + if (!string.IsNullOrEmpty(Province)) + address += string.Format($" จ.{Province}"); + + return address; + } + } +} diff --git a/ThaiNationalIDCard.NET/Models/Personal.cs b/ThaiNationalIDCard.NET/Models/Personal.cs new file mode 100644 index 0000000..e037996 --- /dev/null +++ b/ThaiNationalIDCard.NET/Models/Personal.cs @@ -0,0 +1,16 @@ +using System; + +namespace ThaiNationalIDCard.NET.Models +{ + public class Personal + { + public string CitizenID { get; set; } + public PersonalInfo ThaiPersonalInfo { get; set; } + public PersonalInfo EnglishPersonalInfo { get; set; } + public string Sex { get; set; } + public AddressInfo AddressInfo { get; set; } + public DateTime IssueDate { get; set; } + public DateTime ExpireDate { get; set; } + public string Issuer { get; set; } + } +} diff --git a/ThaiNationalIDCard.NET/Models/PersonalInfo.cs b/ThaiNationalIDCard.NET/Models/PersonalInfo.cs new file mode 100644 index 0000000..db022fa --- /dev/null +++ b/ThaiNationalIDCard.NET/Models/PersonalInfo.cs @@ -0,0 +1,25 @@ +namespace ThaiNationalIDCard.NET.Models +{ + public class PersonalInfo + { + public string Prefix { get; set; } + public string FirstName { get; set; } + public string MiddleName { get; set; } + public string LastName { get; set; } + + public PersonalInfo(string personalInfo) + { + string[] infos = personalInfo.Split('#'); + + Prefix = infos[0].Trim(); + FirstName = infos[1].Trim(); + MiddleName = infos[2].Trim(); + LastName = infos[3].Trim(); + } + + public override string ToString() + { + return string.Format($"{Prefix}{FirstName} {LastName}"); + } + } +} diff --git a/ThaiNationalIDCard.NET/Models/PersonalPhoto.cs b/ThaiNationalIDCard.NET/Models/PersonalPhoto.cs new file mode 100644 index 0000000..5cac0bc --- /dev/null +++ b/ThaiNationalIDCard.NET/Models/PersonalPhoto.cs @@ -0,0 +1,19 @@ +namespace ThaiNationalIDCard.NET.Models +{ + public class PersonalPhoto : Personal + { + public string Photo { get; set; } + + public PersonalPhoto(Personal personal) + { + CitizenID = personal.CitizenID; + ThaiPersonalInfo = personal.ThaiPersonalInfo; + EnglishPersonalInfo = personal.EnglishPersonalInfo; + Sex = personal.Sex; + AddressInfo = personal.AddressInfo; + IssueDate = personal.IssueDate; + ExpireDate = personal.ExpireDate; + Issuer = personal.Issuer; + } + } +} diff --git a/ThaiNationalIDCard.NET/Models/ThaiNationalIDCardAPDUCommand.cs b/ThaiNationalIDCard.NET/Models/ThaiNationalIDCardAPDUCommand.cs new file mode 100644 index 0000000..9564b3f --- /dev/null +++ b/ThaiNationalIDCard.NET/Models/ThaiNationalIDCardAPDUCommand.cs @@ -0,0 +1,53 @@ +using System.Linq; +using ThaiNationalIDCard.NET.Interfaces; + +namespace ThaiNationalIDCard.NET.Models +{ + public abstract class ThaiNationalIDCardAPDUCommand : IThaiNationalIDCardAPDUCommand + { + public byte[] MinistryOfInteriorAppletCommand => new byte[] { 0xA0, 0X00, 0x00, 0x00, 0x54, 0x48, 0x00, 0x01 }; + + public byte[] CitizenIDCommand => new byte[] { 0x80, 0xb0, 0x00, 0x04, 0x02, 0x00, 0x0d }; + + public byte[] PersonalInfoCommand => new byte[] { 0x80, 0xb0, 0x00, 0x11, 0x02, 0x00, 0xd1 }; + + public byte[] AddressInfoCommand => new byte[] { 0x80, 0xb0, 0x15, 0x79, 0x02, 0x00, 0x64 }; + + public byte[] CardIssueExpireCommand => new byte[] { 0x80, 0xb0, 0x01, 0x67, 0x02, 0x00, 0x12 }; + + public byte[] CardIssuerCommand => new byte[] { 0x80, 0xb0, 0x00, 0xf6, 0x02, 0x00, 0x64 }; + + public byte[][] PhotoCommand => new byte[][] + { + new byte[]{ 0x80, 0xB0, 0x01, 0x7B, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x02, 0x7A, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x03, 0x79, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x04, 0x78, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x05, 0x77, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x06, 0x76, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x07, 0x75, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x08, 0x74, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x09, 0x73, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x0A, 0x72, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x0B, 0x71, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x0C, 0x70, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x0D, 0x6F, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x0E, 0x6E, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x0F, 0x6D, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x10, 0x6C, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x11, 0x6B, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x12, 0x6A, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x13, 0x69, 0x02, 0x00, 0xFF }, + new byte[]{ 0x80, 0xB0, 0x14, 0x68, 0x02, 0x00, 0xFF }, + }; + + public abstract byte[] GetResponse(); + + public byte[] Select(byte[] command) + { + byte[] selectCommand = new byte[] { 0x00, 0xA4, 0x04, 0x00, (byte)command.Length }; + + return selectCommand.Concat(command).ToArray(); + } + } +} diff --git a/ThaiNationalIDCard.NET/Models/ThaiNationalIDCardAPDUCommandType01.cs b/ThaiNationalIDCard.NET/Models/ThaiNationalIDCardAPDUCommandType01.cs new file mode 100644 index 0000000..b80de73 --- /dev/null +++ b/ThaiNationalIDCard.NET/Models/ThaiNationalIDCardAPDUCommandType01.cs @@ -0,0 +1,10 @@ +namespace ThaiNationalIDCard.NET.Models +{ + public class ThaiNationalIDCardAPDUCommandType01 : ThaiNationalIDCardAPDUCommand + { + public override byte[] GetResponse() + { + return new byte[] { 0x00, 0xc0, 0x00, 0x01 }; + } + } +} diff --git a/ThaiNationalIDCard.NET/Models/ThaiNationalIDCardAPDUCommandType02.cs b/ThaiNationalIDCard.NET/Models/ThaiNationalIDCardAPDUCommandType02.cs new file mode 100644 index 0000000..694f508 --- /dev/null +++ b/ThaiNationalIDCard.NET/Models/ThaiNationalIDCardAPDUCommandType02.cs @@ -0,0 +1,10 @@ +namespace ThaiNationalIDCard.NET.Models +{ + class ThaiNationalIDCardAPDUCommandType02 : ThaiNationalIDCardAPDUCommand + { + public override byte[] GetResponse() + { + return new byte[] { 0x00, 0xc0, 0x00, 0x00 }; + } + } +} diff --git a/ThaiNationalIDCard.NET/ThaiNationalIDCard.NET.csproj b/ThaiNationalIDCard.NET/ThaiNationalIDCard.NET.csproj new file mode 100644 index 0000000..be259c8 --- /dev/null +++ b/ThaiNationalIDCard.NET/ThaiNationalIDCard.NET.csproj @@ -0,0 +1,20 @@ + + + + netstandard2.0 + Pichid Detson + ThaiNationalIDCard.NET + Thai National ID Card Reader + true + Thai National ID Card Reader for .NET + https://github.com/bencomtech/ThaiNationalIDCard.NET + https://github.com/bencomtech/ThaiNationalIDCard.NET + + + + + + + + + diff --git a/ThaiNationalIDCard.NET/ThaiNationalIDCardReader.cs b/ThaiNationalIDCard.NET/ThaiNationalIDCardReader.cs new file mode 100644 index 0000000..add5cee --- /dev/null +++ b/ThaiNationalIDCard.NET/ThaiNationalIDCardReader.cs @@ -0,0 +1,240 @@ +using PCSC; +using PCSC.Exceptions; +using PCSC.Iso7816; +using PCSC.Utils; +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using ThaiNationalIDCard.NET.Interfaces; +using ThaiNationalIDCard.NET.Models; + +namespace ThaiNationalIDCard.NET +{ + public class ThaiNationalIDCardReader + { + private readonly ISCardContext context; + private readonly ISCardReader reader; + + private SCardError error; + private IntPtr intPtr; + private IThaiNationalIDCardAPDUCommand apdu; + + public ThaiNationalIDCardReader() + { + context = ContextFactory.Instance.Establish(SCardScope.System); + reader = new SCardReader(context); + apdu = new ThaiNationalIDCardAPDUCommandType02(); + + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + } + + public Personal GetPersonal() + { + try + { + Open(); + + return GetPersonalInfo(); + } + catch (Exception e) + { + throw e; + } + finally + { + Close(); + } + } + + public PersonalPhoto GetPersonalPhoto() + { + MemoryStream stream = new MemoryStream(); + + try + { + Open(); + + Personal personal = GetPersonalInfo(); + PersonalPhoto personalPhoto = new PersonalPhoto(personal); + + byte[][] photoCommand = apdu.PhotoCommand; + byte[] responseBuffer; + + for (int i = 0; i < photoCommand.Length; i++) + { + responseBuffer = SendCommand(photoCommand[i]); + stream.Write(responseBuffer, 0, responseBuffer.Length); + } + + stream.Seek(0, SeekOrigin.Begin); + + personalPhoto.Photo = string.Format($"data:image/jpeg;base64,{Convert.ToBase64String(stream.ToArray())}"); + + return personalPhoto; + } + catch (Exception e) + { + throw e; + } + finally + { + stream.Dispose(); + + Close(); + } + } + + private Personal GetPersonalInfo() + { + try + { + Personal personal = new Personal(); + + personal.CitizenID = GetUTF8FromAsciiBytes(SendCommand(apdu.CitizenIDCommand)); + + string personalInfo = GetUTF8FromAsciiBytes(SendCommand(apdu.PersonalInfoCommand)); + string thaiPersonalInfo = personalInfo.Substring(0, 100); + string englishPersonalInfo = personalInfo.Substring(100, 100); + + personal.ThaiPersonalInfo = new PersonalInfo(thaiPersonalInfo); + personal.EnglishPersonalInfo = new PersonalInfo(englishPersonalInfo); + personal.Sex = personalInfo.Substring(208, 1); + + string addressInfo = GetUTF8FromAsciiBytes(SendCommand(apdu.AddressInfoCommand)); + + personal.AddressInfo = new AddressInfo(addressInfo); + + string cardIssueExpire = GetUTF8FromAsciiBytes(SendCommand(apdu.CardIssueExpireCommand)); + + personal.IssueDate = new DateTime(Convert.ToInt32(cardIssueExpire.Substring(0, 4)) - 543 + , Convert.ToInt32(cardIssueExpire.Substring(4, 2)) + , Convert.ToInt32(cardIssueExpire.Substring(6, 2)) + ); + personal.ExpireDate = new DateTime(Convert.ToInt32(cardIssueExpire.Substring(8, 4)) - 543 + , Convert.ToInt32(cardIssueExpire.Substring(12, 2)) + , Convert.ToInt32(cardIssueExpire.Substring(14, 2)) + ); + personal.Issuer = GetUTF8FromAsciiBytes(SendCommand(apdu.CardIssuerCommand)); + + return personal; + } + catch (Exception e) + { + throw e; + } + } + + private string GetUTF8FromAsciiBytes(byte[] asciiBytes) + { + byte[] utf8 = Encoding.Convert( + Encoding.GetEncoding("TIS-620"), + Encoding.UTF8, + asciiBytes + ); + + return Encoding.UTF8.GetString(utf8); + } + + private byte[] SendCommand(byte[] command) + { + byte[] responseBuffer = new byte[256]; + + error = reader.Transmit(intPtr, command, ref responseBuffer); + HandleError(); + + ResponseApdu responseApdu = new ResponseApdu(responseBuffer, IsoCase.Case2Short, reader.ActiveProtocol); + + if (responseApdu.SW1.Equals((byte)SW1Code.NormalDataResponse)) + { + command = apdu.GetResponse().Concat(new byte[] { responseApdu.SW2 }).ToArray(); + responseBuffer = new byte[258]; + + error = reader.Transmit(intPtr, command, ref responseBuffer); + HandleError(); + + if (responseBuffer.Length - responseApdu.SW2 == 2) + { + return responseBuffer.Take(responseBuffer.Length - 2).ToArray(); + } + } + + return responseBuffer; + } + + private void Close() + { + reader.Disconnect(SCardReaderDisposition.Leave); + context.Release(); + } + + private void Open() + { + try + { + Thread.Sleep(1500); + + string[] szReaders = context.GetReaders(); + if (szReaders.Length <= 0) + throw new PCSCException(SCardError.NoReadersAvailable, "Could not find any Smartcard reader."); + + error = reader.Connect(szReaders[0], SCardShareMode.Shared, SCardProtocol.T0 | SCardProtocol.T1); + HandleError(); + + intPtr = new IntPtr(); + switch (reader.ActiveProtocol) + { + case SCardProtocol.T0: + intPtr = SCardPCI.T0; + break; + case SCardProtocol.T1: + intPtr = SCardPCI.T1; + break; + case SCardProtocol.Raw: + intPtr = SCardPCI.Raw; + break; + default: + throw new PCSCException(SCardError.ProtocolMismatch, "Protocol not supported: " + reader.ActiveProtocol.ToString()); + } + + error = reader.Status(out string[] readerNames, out SCardState state, out SCardProtocol protocol, out byte[] atrs); + HandleError(); + + if (atrs == null || atrs.Length < 2) + throw new PCSCException(SCardError.InvalidAtr); + + if (atrs[0] == 0x3B && atrs[1] == 0x67) + apdu = new ThaiNationalIDCardAPDUCommandType01(); + + if (!SelectApplet()) + throw new Exception("SmartCard not support (Can't select Ministry of Interior Applet)."); + } + catch (PCSCException e) + { + throw e; + } + } + + private bool SelectApplet() + { + byte[] command = apdu.Select(apdu.MinistryOfInteriorAppletCommand); + byte[] responseBuffer = new byte[256]; + + error = reader.Transmit(intPtr, command, ref responseBuffer); + HandleError(); + + ResponseApdu responseApdu = new ResponseApdu(responseBuffer, IsoCase.Case2Short, reader.ActiveProtocol); + + return responseApdu.SW1.Equals((byte)SW1Code.NormalDataResponse) || responseApdu.SW1.Equals((byte)SW1Code.Normal); + } + + private void HandleError() + { + if (error == SCardError.Success) + return; + + throw new PCSCException(error, SCardHelper.StringifyError(error)); + } + } +} diff --git a/cleanup-obj-bin.ps1 b/cleanup-obj-bin.ps1 new file mode 100644 index 0000000..3ba7e3e --- /dev/null +++ b/cleanup-obj-bin.ps1 @@ -0,0 +1 @@ +Get-ChildItem .\ -include bin,obj -Recurse | foreach ($_) { remove-item $_.fullname -Force -Recurse } \ No newline at end of file