commit 71d32d6ad12f07b00dda50af634a8d9c8b8aba1c Author: nat Date: Wed Jan 10 07:31:47 2024 -0800 aimeio-pcsc: vcs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..600d2d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode \ No newline at end of file diff --git a/aimeio/aimeio.c b/aimeio/aimeio.c new file mode 100644 index 0000000..2cb9b3c --- /dev/null +++ b/aimeio/aimeio.c @@ -0,0 +1,112 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "aimeio/aimeio.h" +#include "aimepcsc/aimepcsc.h" + +static struct aimepcsc_context *ctx; +static struct aime_data data; + +uint16_t aime_io_get_api_version(void) +{ + return 0x0100; +} + +HRESULT aime_io_init(void) +{ + int ret; + + AllocConsole(); + SetConsoleTitle("aimeio-pcsc"); + FILE* fp; + freopen_s(&fp, "CONOUT$", "w", stdout); + + ctx = aimepcsc_create(); + if (!ctx) { + return E_OUTOFMEMORY; + } + + ret = aimepcsc_init(ctx); + + if (ret != 0) { + printf("aimeio-pcsc: failed to initialize: %s\n", aimepcsc_error(ctx)); + aimepcsc_destroy(ctx); + ctx = NULL; + return E_FAIL; + } + + printf("aimeio-pcsc: initialized with reader: %s\n", aimepcsc_reader_name(ctx)); + + return S_OK; +} + +HRESULT aime_io_nfc_poll(uint8_t unit_no) +{ + int ret; + HRESULT hr; + + if (unit_no != 0) { + return S_OK; + } + + memset(&data, 0, sizeof(data)); + printf("aimeio-pcsc: poll.\n"); + + ret = aimepcsc_poll(ctx, &data); + + if (ret < 0) { + printf("aimeio-pcsc: poll failed: %s\n", aimepcsc_error(ctx)); + } + + return S_OK; +} + +HRESULT aime_io_nfc_get_aime_id( + uint8_t unit_no, + uint8_t *luid, + size_t luid_size) +{ + assert(luid != NULL); + + if (unit_no != 0 || data.card_type != Mifare) { + return S_FALSE; + } + + assert(luid_size == data.card_id_len); + + memcpy(luid, data.card_id, luid_size); + + return S_OK; +} + +HRESULT aime_io_nfc_get_felica_id(uint8_t unit_no, uint64_t *IDm) +{ + uint64_t val; + size_t i; + + assert(IDm != NULL); + + if (unit_no != 0 || data.card_type != FeliCa) { + return S_FALSE; + } + + val = 0; + + for (i = 0 ; i < 8 ; i++) { + val = (val << 8) | data.card_id[i]; + } + + *IDm = val; + + return S_OK; +} + +void aime_io_led_set_color(uint8_t unit_no, uint8_t r, uint8_t g, uint8_t b) +{} diff --git a/aimeio/aimeio.h b/aimeio/aimeio.h new file mode 100644 index 0000000..221f8e0 --- /dev/null +++ b/aimeio/aimeio.h @@ -0,0 +1,87 @@ +#pragma once + +#include + +#include +#include + +/* + Get the version of the Aime IO API that this DLL supports. This function + should return a positive 16-bit integer, where the high byte is the major + version and the low byte is the minor version (as defined by the Semantic + Versioning standard). + + The latest API version as of this writing is 0x0100. + */ +uint16_t aime_io_get_api_version(void); + +/* + Initialize Aime IO provider DLL. Only called once, before any other + functions exported from this DLL are called (except for + aime_io_get_api_version). + + Minimum API version: 0x0100 + */ +HRESULT aime_io_init(void); + +/* + Poll for IC cards in the vicinity. + + - unit_no: Always 0 as of the current API version + + Minimum API version: 0x0100 + */ +HRESULT aime_io_nfc_poll(uint8_t unit_no); + +/* + Attempt to read out a classic Aime card ID + + - unit_no: Always 0 as of the current API version + - luid: Pointer to a ten-byte buffer that will receive the ID + - luid_size: Size of the buffer at *luid. Always 10. + + Returns: + + - S_OK if a classic Aime is present and was read successfully + - S_FALSE if no classic Aime card is present (*luid will be ignored) + - Any HRESULT error if an error occured. + + Minimum API version: 0x0100 +*/ +HRESULT aime_io_nfc_get_aime_id( + uint8_t unit_no, + uint8_t *luid, + size_t luid_size); + +/* + Attempt to read out a FeliCa card ID ("IDm"). The following are examples + of FeliCa cards: + + - Amuse IC (which includes new-style Aime-branded cards, among others) + - Smartphones with FeliCa NFC capability (uncommon outside Japan) + - Various Japanese e-cash cards and train passes + + Parameters: + + - unit_no: Always 0 as of the current API version + - IDm: Output parameter that will receive the card ID + + Returns: + + - S_OK if a FeliCa device is present and was read successfully + - S_FALSE if no FeliCa device is present (*IDm will be ignored) + - Any HRESULT error if an error occured. + + Minimum API version: 0x0100 +*/ +HRESULT aime_io_nfc_get_felica_id(uint8_t unit_no, uint64_t *IDm); + +/* + Change the color and brightness of the card reader's RGB lighting + + - unit_no: Always 0 as of the current API version + - r, g, b: Primary color intensity, from 0 to 255 inclusive. + + Minimum API version: 0x0100 +*/ +void aime_io_led_set_color(uint8_t unit_no, uint8_t r, uint8_t g, uint8_t b); diff --git a/aimeio/meson.build b/aimeio/meson.build new file mode 100644 index 0000000..2bb1fa8 --- /dev/null +++ b/aimeio/meson.build @@ -0,0 +1,10 @@ +aimeio_lib = shared_library( + 'aimeio', + name_prefix : '', + include_directories: inc, + implicit_include_directories : false, + link_with: [aimepcsc_lib], + sources : [ + 'aimeio.c', + ], +) diff --git a/aimepcsc/aimepcsc.c b/aimepcsc/aimepcsc.c new file mode 100644 index 0000000..fbea106 --- /dev/null +++ b/aimepcsc/aimepcsc.c @@ -0,0 +1,222 @@ +#include "aimepcsc.h" +#include + +static const uint8_t atr_ios14443_common[] = {0x3B, 0x8F, 0x80, 0x01, 0x80, 0x4F, 0x0C, 0xA0, 0x00, 0x00, 0x03, 0x06}; +static const uint8_t cardtype_m1k[] = {0x03, 0x00, 0x01}; +static const uint8_t cardtype_felica[] = {0x11, 0x00, 0x3B}; + +static const uint8_t m1k_cmd_loadkey[] = {0xFF, 0x82, 0x00, 0x00, 0x06, 0x57, 0x43, 0x43, 0x46, 0x76, 0x32}; +static const uint8_t m1k_cmd_auth_block2[] = {0xFF, 0x86, 0x00, 0x00, 0x05, 0x01, 0x00, 0x02, 0x61, 0x00}; +static const uint8_t m1k_cmd_read_block2[] = {0xFF, 0xB0, 0x00, 0x02, 0x10}; + +struct aimepcsc_context { + SCARDCONTEXT hContext; + LPSTR mszReaders; + DWORD pcchReaders; + + CHAR last_error[256]; +}; + +static int read_felica_aime(struct aimepcsc_context *ctx, LPSCARDHANDLE card, struct aime_data *data) { + uint8_t buf[32]; + DWORD len; + LONG ret; + + /* read card ID */ + len = sizeof(buf); + ret = SCardTransmit(*card, SCARD_PCI_T1, (LPCBYTE) "\xFF\xCA\x00\x00\x00", 5, NULL, buf, &len); + + if (ret != SCARD_S_SUCCESS) { + snprintf(ctx->last_error, sizeof(ctx->last_error), "SCardTransmit failed: %08lX", (ULONG) ret); + return -1; + } + + if (len != 10) { + snprintf(ctx->last_error, sizeof(ctx->last_error), "invalid card ID length: %lu", len); + return -1; + } + + data->card_id_len = 8; + memcpy(data->card_id, buf, 8); + + return 0; +} + +#define M1K_CMD(card, cmd, expected_res_len) \ + do { \ + len = sizeof(buf); \ + ret = SCardTransmit(*card, SCARD_PCI_T1, cmd, sizeof(cmd), NULL, buf, &len); \ + if (ret != SCARD_S_SUCCESS) { \ + snprintf(ctx->last_error, sizeof(ctx->last_error), "SCardTransmit failed during " #cmd ": %08lX", (ULONG) ret); \ + return -1; \ + } \ + if (len != expected_res_len || buf[expected_res_len - 2] != 0x90 || buf[expected_res_len - 1] != 0x00) { \ + snprintf(ctx->last_error, sizeof(ctx->last_error), #cmd " failed; res_len=%lu, res_code=%02x%02x", len, buf[2], buf[3]); \ + return -1; \ + } \ + } while (0) + +static int read_m1k_aime(struct aimepcsc_context *ctx, LPSCARDHANDLE card, struct aime_data *data) { + uint8_t buf[32]; + DWORD len; + LONG ret; + + /* load key onto reader */ + M1K_CMD(card, m1k_cmd_loadkey, 2); + + /* authenticate block 2 */ + M1K_CMD(card, m1k_cmd_auth_block2, 2); + + /* read block 2 */ + M1K_CMD(card, m1k_cmd_read_block2, 18); + + data->card_id_len = 10; + memcpy(data->card_id, buf + 6, 10); + + return 0; +} + +struct aimepcsc_context* aimepcsc_create(void) { + struct aimepcsc_context* ctx; + + ctx = (struct aimepcsc_context*) malloc(sizeof(struct aimepcsc_context)); + if (!ctx) { + return NULL; + } + + memset(ctx, 0, sizeof(struct aimepcsc_context)); + return ctx; +} + +void aimepcsc_destroy(struct aimepcsc_context *ctx) { + free(ctx); +} + +int aimepcsc_init(struct aimepcsc_context *ctx) { + LONG ret; + + ret = SCardEstablishContext(SCARD_SCOPE_USER, NULL, NULL, &ctx->hContext); + + if (ret != SCARD_S_SUCCESS) { + snprintf(ctx->last_error, sizeof(ctx->last_error), "SCardEstablishContext failed: %08lX", (ULONG) ret); + return -1; + } + + ctx->pcchReaders = SCARD_AUTOALLOCATE; + + ret = SCardListReaders(ctx->hContext, NULL, (LPSTR) &ctx->mszReaders, &ctx->pcchReaders); + + if (ret != SCARD_S_SUCCESS) { + snprintf(ctx->last_error, sizeof(ctx->last_error), "SCardListReaders failed: %08lX", (ULONG) ret); + goto errout; + } + + return 0; + +errout: + SCardReleaseContext(ctx->hContext); + return -1; +} + +void aimepcsc_shutdown(struct aimepcsc_context *ctx) { + if (ctx->mszReaders != NULL) { + SCardFreeMemory(ctx->hContext, ctx->mszReaders); + } + + SCardReleaseContext(ctx->hContext); +} + +int aimepcsc_poll(struct aimepcsc_context *ctx, struct aime_data *data) { + SCARDHANDLE hCard; + SCARD_READERSTATE rs; + LONG ret; + LPBYTE pbAttr = NULL; + DWORD cByte = SCARD_AUTOALLOCATE; + + memset(&rs, 0, sizeof(SCARD_READERSTATE)); + + rs.szReader = ctx->mszReaders; + rs.dwCurrentState = SCARD_STATE_UNAWARE; + + /* check if a card is present */ + ret = SCardGetStatusChange(ctx->hContext, 0, &rs, 1); + + if (ret == SCARD_E_TIMEOUT) { + return 1; + } + + if (ret != SCARD_S_SUCCESS) { + snprintf(ctx->last_error, sizeof(ctx->last_error), "SCardGetStatusChange failed: %08lX", (ULONG) ret); + return -1; + } + + if (rs.dwEventState & SCARD_STATE_EMPTY) { + return 1; + } + + if (!(rs.dwEventState & SCARD_STATE_PRESENT)) { + sprintf(ctx->last_error, "unknown dwCurrentState: %08lX", rs.dwCurrentState); + return -1; + } + + /* connect to card */ + ret = SCardConnect(ctx->hContext, rs.szReader, SCARD_SHARE_SHARED, SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, &hCard, NULL); + + if (ret != SCARD_S_SUCCESS) { + snprintf(ctx->last_error, sizeof(ctx->last_error), "SCardConnect failed: %08lX", (ULONG) ret); + return -1; + } + + /* get ATR string */ + ret = SCardGetAttrib(hCard, SCARD_ATTR_ATR_STRING, (LPBYTE) &pbAttr, &cByte); + + if (ret != SCARD_S_SUCCESS) { + snprintf(ctx->last_error, sizeof(ctx->last_error), "SCardGetAttrib failed: %08lX", (ULONG) ret); + return -1; + } + + if (cByte != 20) { + snprintf(ctx->last_error, sizeof(ctx->last_error), "invalid ATR length: %lu", cByte); + goto errout; + } + + /* check ATR */ + if (memcmp(pbAttr, atr_ios14443_common, sizeof(atr_ios14443_common)) != 0) { + snprintf(ctx->last_error, sizeof(ctx->last_error), "invalid card type."); + goto errout; + } + + /* check card type */ + if (memcmp(pbAttr + sizeof(atr_ios14443_common), cardtype_m1k, sizeof(cardtype_m1k)) == 0) { + data->card_type = Mifare; + if (read_m1k_aime(ctx, &hCard, data) != 0) { + goto errout; + } + } else if (memcmp(pbAttr + sizeof(atr_ios14443_common), cardtype_felica, sizeof(cardtype_felica)) == 0) { + data->card_type = FeliCa; + if (read_felica_aime(ctx, &hCard, data) != 0) { + goto errout; + } + } else { + snprintf(ctx->last_error, sizeof(ctx->last_error), "invalid card type."); + goto errout; + } + + SCardFreeMemory(ctx->hContext, pbAttr); + SCardDisconnect(hCard, SCARD_LEAVE_CARD); + + return 0; + +errout: + SCardFreeMemory(ctx->hContext, pbAttr); + SCardDisconnect(hCard, SCARD_LEAVE_CARD); + return -1; +} + +const char* aimepcsc_error(struct aimepcsc_context *ctx) { + return ctx->last_error; +} + +const char* aimepcsc_reader_name(struct aimepcsc_context *ctx) { + return ctx->mszReaders; +} \ No newline at end of file diff --git a/aimepcsc/aimepcsc.h b/aimepcsc/aimepcsc.h new file mode 100644 index 0000000..934abea --- /dev/null +++ b/aimepcsc/aimepcsc.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include + +#include + +/* opaque context */ +struct aimepcsc_context; + +/* aime card types */ +enum AIME_CARDTYPE { + Mifare = 0x01, + FeliCa = 0x02, +}; + +/* aime card data */ +struct aime_data { + /* AIME_CARDTYPE */ + uint8_t card_type; + + /* Card ID */ + uint8_t card_id[32]; + + /* Card ID length */ + uint8_t card_id_len; +}; + +/* create new context for aimepcsc + * @return context on success, NULL on failure + */ +struct aimepcsc_context* aimepcsc_create(void); + +/* destroy context for aimepcsc */ +void aimepcsc_destroy(struct aimepcsc_context *ctx); + +/* setup reader (first detected reader will be used) + * @param ctx context + * @return 0 on success, -1 on failure + */ +int aimepcsc_init(struct aimepcsc_context *ctx); + +/* shutdown reader */ +void aimepcsc_shutdown(struct aimepcsc_context *ctx); + +/* poll for card + * @param ctx context + * @param data data to be filled + * @return 0 on success, -1 on failure, 1 on no card + */ +int aimepcsc_poll(struct aimepcsc_context *ctx, struct aime_data *data); + +/* get last error + * @param ctx context + * @return error string + */ +const char* aimepcsc_error(struct aimepcsc_context *ctx); + +/* get reader name + * @param ctx context + * @return reader name + */ +const char* aimepcsc_reader_name(struct aimepcsc_context *ctx); \ No newline at end of file diff --git a/aimepcsc/aimereader.c b/aimepcsc/aimereader.c new file mode 100644 index 0000000..73346c7 --- /dev/null +++ b/aimepcsc/aimereader.c @@ -0,0 +1,44 @@ +#include "aimepcsc.h" +#include + +int main(int argc, char** argv) { + struct aimepcsc_context* ctx; + int ret; + + ctx = aimepcsc_create(); + if (!ctx) { + return -1; + } + + ret = aimepcsc_init(ctx); + if (ret != 0) { + fprintf(stderr, "aimepcsc_init failed: %s\n", aimepcsc_error(ctx)); + return -1; + } + + fprintf(stderr, "connected to reader: %s; waiting for cards...\n", aimepcsc_reader_name(ctx)); + + while (1) { + struct aime_data data; + + ret = aimepcsc_poll(ctx, &data); + if (ret == 0) { + printf("card detected: "); + printf("type=%s, id=", data.card_type == Mifare ? "Mifare (old Aime)" : "FeliCa (AIC-Aime)"); + for (int i = 0; i < data.card_id_len; i++) { + printf("%02X", data.card_id[i]); + } + printf("\n"); + } else if (ret == -1) { + fprintf(stderr, "aimepcsc_poll failed: %s\n", aimepcsc_error(ctx)); + break; + } + + Sleep(500); + } + + aimepcsc_shutdown(ctx); + aimepcsc_destroy(ctx); + + return 0; +} \ No newline at end of file diff --git a/aimepcsc/meson.build b/aimepcsc/meson.build new file mode 100644 index 0000000..67e3751 --- /dev/null +++ b/aimepcsc/meson.build @@ -0,0 +1,23 @@ +winscard_lib = cc.find_library('winscard') + +aimepcsc_lib = static_library( + 'aimepcsc', + name_prefix : '', + include_directories: inc, + implicit_include_directories : false, + dependencies: [winscard_lib], + sources : [ + 'aimepcsc.c', + ], +) + +aimereader = executable( + 'aimereader', + name_prefix : '', + include_directories : [inc], + implicit_include_directories : false, + link_with: [aimepcsc_lib], + sources : [ + 'aimereader.c' + ] +) diff --git a/cross-mingw-32.txt b/cross-mingw-32.txt new file mode 100644 index 0000000..21ed951 --- /dev/null +++ b/cross-mingw-32.txt @@ -0,0 +1,10 @@ +[binaries] +c = 'i686-w64-mingw32-gcc' +ar = 'i686-w64-mingw32-ar' +strip = 'i686-w64-mingw32-strip' + +[host_machine] +system = 'windows' +cpu_family = 'x86' +cpu = 'i686' +endian = 'little' diff --git a/cross-mingw-64.txt b/cross-mingw-64.txt new file mode 100644 index 0000000..6b15798 --- /dev/null +++ b/cross-mingw-64.txt @@ -0,0 +1,10 @@ +[binaries] +c = 'x86_64-w64-mingw32-gcc' +ar = 'x86_64-w64-mingw32-ar' +strip = 'x86_64-w64-mingw32-strip' + +[host_machine] +system = 'windows' +cpu_family = 'x86_64' +cpu = 'x86_64' +endian = 'little' diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..26c1063 --- /dev/null +++ b/meson.build @@ -0,0 +1,44 @@ +project( + 'aimeio-pcsc', + 'c', + version: '0.0.1', + default_options: [ + 'warning_level=3', + ], +) + +add_project_arguments( + '-DCOBJMACROS', + '-DDIRECTINPUT_VERSION=0x0800', + '-DWIN32_LEAN_AND_MEAN', + '-D_WIN32_WINNT=_WIN32_WINNT_WIN7', + '-DMINGW_HAS_SECURE_API=1', + '-Wno-unused', + language: 'c', +) + +cc = meson.get_compiler('c') + +if cc.get_id() != 'msvc' + add_project_arguments( + '-ffunction-sections', + '-fdata-sections', + '-flto', # Enable Link-Time Optimization + language: 'c', + ) + + add_project_link_arguments( + '-Wl,--enable-stdcall-fixup', + '-Wl,--exclude-all-symbols', + '-Wl,--gc-sections', + '-static-libgcc', + '-flto', # Enable Link-Time Optimization + '-Wl,-s', # Strip debug symbols + language: 'c', + ) +endif + +inc = include_directories('.') + +subdir('aimepcsc') +subdir('aimeio') diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..651730b --- /dev/null +++ b/readme.md @@ -0,0 +1,28 @@ +aimeio-pcsc +--- + +PC/SC-based Aime card reader for `segatools`. `aimeio-pcsc` allows you to use PC/SC compliant smart card readers as your Aime card reader in `segatools`. `aimeio-pcsc` only properly supports the old Mifare Classic 1K-based Aime cards. + +If you scan a newer AIC-based AIME, its FeliCa IDm will be provided to the game. The game will not see the correct "access code," but the IDm should be unique to each card so that particular card can still track your plays. + +Tested on SONY's PaSoRi RC-S300. Other readers should, in theory, also work. + +### Usage + +To test if your card reader is supported, run `aimereader.exe` and try read your Aime card. + +To use it with a game, copy `aimeio.dll` to your `segatools` folder and add the following to your `segatools.ini`: + +```ini +[aimeio] +path=aimeio.dll +``` + +### Build + +On Linux: + +```sh +meson setup --cross cross-mingw-64.txt b64 +ninja -C b64 +``` \ No newline at end of file