Merge branch 'feature/thinca_auth' into develop
This commit is contained in:
@ -75,6 +75,15 @@ void aime_config_load(struct aime_config *cfg, const wchar_t *filename)
|
||||
cfg->port_no = GetPrivateProfileIntW(L"aime", L"portNo", 0, filename);
|
||||
cfg->high_baudrate = GetPrivateProfileIntW(L"aime", L"highBaud", 1, filename);
|
||||
cfg->gen = GetPrivateProfileIntW(L"aime", L"gen", 0, filename);
|
||||
cfg->proxy_flag = GetPrivateProfileIntW(L"aime", L"proxyFlag", 2, filename);
|
||||
|
||||
GetPrivateProfileStringW(
|
||||
L"aime",
|
||||
L"authdataPath",
|
||||
L"DEVICE\\authdata.bin",
|
||||
cfg->authdata_path,
|
||||
_countof(cfg->authdata_path),
|
||||
filename);
|
||||
}
|
||||
|
||||
void io4_config_load(struct io4_config *cfg, const wchar_t *filename)
|
||||
|
@ -12,10 +12,10 @@ enum {
|
||||
SG_NFC_CMD_POLL = 0x42,
|
||||
SG_NFC_CMD_MIFARE_SELECT_TAG = 0x43,
|
||||
SG_NFC_CMD_MIFARE_SET_KEY_AIME = 0x50,
|
||||
SG_NFC_CMD_MIFARE_AUTHENTICATE_A = 0x51,
|
||||
SG_NFC_CMD_MIFARE_AUTHENTICATE_AIME = 0x51,
|
||||
SG_NFC_CMD_MIFARE_READ_BLOCK = 0x52,
|
||||
SG_NFC_CMD_MIFARE_SET_KEY_BANA = 0x54,
|
||||
SG_NFC_CMD_MIFARE_AUTHENTICATE_B = 0x55,
|
||||
SG_NFC_CMD_MIFARE_AUTHENTICATE_BANA = 0x55,
|
||||
SG_NFC_CMD_TO_UPDATE_MODE = 0x60,
|
||||
SG_NFC_CMD_SEND_HEX_DATA = 0x61,
|
||||
SG_NFC_CMD_RESET = 0x62,
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
@ -16,6 +17,7 @@
|
||||
|
||||
#include "util/dprintf.h"
|
||||
#include "util/dump.h"
|
||||
#include "util/slurp.h"
|
||||
|
||||
static HRESULT sg_nfc_dispatch(
|
||||
void *ctx,
|
||||
@ -87,6 +89,8 @@ void sg_nfc_init(
|
||||
uint8_t addr,
|
||||
const struct sg_nfc_ops *ops,
|
||||
unsigned int gen,
|
||||
unsigned int proxy_flag,
|
||||
const wchar_t* authdata_path,
|
||||
void *ops_ctx)
|
||||
{
|
||||
assert(nfc != NULL);
|
||||
@ -96,6 +100,8 @@ void sg_nfc_init(
|
||||
nfc->ops_ctx = ops_ctx;
|
||||
nfc->addr = addr;
|
||||
nfc->gen = gen;
|
||||
nfc->proxy_flag = proxy_flag;
|
||||
nfc->authdata_path = authdata_path;
|
||||
}
|
||||
|
||||
#ifdef NDEBUG
|
||||
@ -189,8 +195,8 @@ static HRESULT sg_nfc_dispatch(
|
||||
&req->felica_encap,
|
||||
&res->felica_encap);
|
||||
|
||||
case SG_NFC_CMD_MIFARE_AUTHENTICATE_A:
|
||||
case SG_NFC_CMD_MIFARE_AUTHENTICATE_B:
|
||||
case SG_NFC_CMD_MIFARE_AUTHENTICATE_AIME:
|
||||
case SG_NFC_CMD_MIFARE_AUTHENTICATE_BANA:
|
||||
case SG_NFC_CMD_SEND_HEX_DATA:
|
||||
return sg_nfc_cmd_send_hex_data(nfc, &req->simple, &res->simple);
|
||||
|
||||
@ -382,17 +388,61 @@ static HRESULT sg_nfc_cmd_mifare_read_block(
|
||||
|
||||
sg_nfc_dprintf(nfc, "Read uid %08x block %i\n", uid, req->payload.block_no);
|
||||
|
||||
if (req->payload.block_no > 3) {
|
||||
if (req->payload.block_no > 14) {
|
||||
sg_nfc_dprintf(nfc, "MIFARE block number out of range\n");
|
||||
|
||||
return E_FAIL;
|
||||
} else if (req->payload.block_no >= 5){ // emoney auth encrypted
|
||||
|
||||
sg_res_init(&res->res, &req->req, sizeof(res->block));
|
||||
|
||||
char* auth;
|
||||
long size = wslurp(nfc->authdata_path, &auth, false);
|
||||
if (size < 0){
|
||||
sg_nfc_dprintf(nfc, "Failed to read %ls: %lx!\n", nfc->authdata_path, GetLastError());
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
int offset = 0;
|
||||
if (req->payload.block_no == 6){
|
||||
offset = 16;
|
||||
} else if (req->payload.block_no == 8){
|
||||
offset = 32;
|
||||
} else if (req->payload.block_no == 9){
|
||||
offset = 48;
|
||||
} else if (req->payload.block_no == 10){
|
||||
offset = 64;
|
||||
} else if (req->payload.block_no == 12){
|
||||
offset = 82;
|
||||
} else if (req->payload.block_no == 13){
|
||||
offset = 98;
|
||||
} else if (req->payload.block_no == 14){
|
||||
offset = 114;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 16 && offset + i < size; i++){
|
||||
res->block[i] = auth[offset + i];
|
||||
}
|
||||
|
||||
free(auth);
|
||||
|
||||
} else if (req->payload.block_no == 4){ // emoney auth plain
|
||||
|
||||
sg_res_init(&res->res, &req->req, sizeof(res->block));
|
||||
|
||||
res->block[0] = 0x54; // header
|
||||
res->block[1] = 0x43;
|
||||
res->block[2] = nfc->proxy_flag; // 2 or 3 depending on game (useProxy in env.json)
|
||||
res->block[3] = 0x01; // unknown flag
|
||||
|
||||
} else { // read all other blocks normally
|
||||
|
||||
sg_res_init(&res->res, &req->req, sizeof(res->block));
|
||||
|
||||
memcpy( res->block,
|
||||
nfc->mifare.sectors[0].blocks[req->payload.block_no].bytes,
|
||||
sizeof(res->block));
|
||||
}
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
@ -23,8 +23,10 @@ struct sg_nfc {
|
||||
void *ops_ctx;
|
||||
uint8_t addr;
|
||||
unsigned int gen;
|
||||
unsigned int proxy_flag;
|
||||
struct felica felica;
|
||||
struct mifare mifare;
|
||||
const wchar_t* authdata_path;
|
||||
};
|
||||
|
||||
void sg_nfc_init(
|
||||
@ -32,6 +34,8 @@ void sg_nfc_init(
|
||||
uint8_t addr,
|
||||
const struct sg_nfc_ops *ops,
|
||||
unsigned int gen,
|
||||
unsigned int proxy_flag,
|
||||
const wchar_t* authdata_path,
|
||||
void *ops_ctx);
|
||||
|
||||
void sg_nfc_transact(
|
||||
|
@ -81,7 +81,7 @@ HRESULT sg_reader_hook_init(
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
|
||||
sg_nfc_init(&sg_reader_nfc, 0x00, &sg_reader_nfc_ops, gen, NULL);
|
||||
sg_nfc_init(&sg_reader_nfc, 0x00, &sg_reader_nfc_ops, gen, cfg->proxy_flag, cfg->authdata_path, NULL);
|
||||
sg_led_init(&sg_reader_led, 0x08, &sg_reader_led_ops, gen, NULL);
|
||||
|
||||
InitializeCriticalSection(&sg_reader_lock);
|
||||
|
@ -12,6 +12,8 @@ struct aime_config {
|
||||
unsigned int port_no;
|
||||
bool high_baudrate;
|
||||
unsigned int gen;
|
||||
unsigned int proxy_flag;
|
||||
wchar_t authdata_path[MAX_PATH];
|
||||
};
|
||||
|
||||
HRESULT sg_reader_hook_init(
|
||||
|
@ -101,6 +101,18 @@ emulates an IC card in its proximity. A variety of different IC cards can be
|
||||
emulated; the exact choice of card that is emulated depends on the presence or
|
||||
absence of the configured card ID files.
|
||||
|
||||
### `proxyFlag`
|
||||
|
||||
Default: `2`
|
||||
|
||||
The "proxy flag" of the emulated Thinca authentication card. This should be 2 if no proxy is used, and 3 if it is. Invalid values will break Thinca authentication card reading. This information can be obtained by checking for the presence of "use_proxy: true" `tfps-res-pro\env.json`.
|
||||
|
||||
### `authdataPath`
|
||||
|
||||
Default: `DEVICE\authdata.bin`
|
||||
|
||||
Path to the binary file containing data for a Thinca authentication card (see `emoney.txt`)
|
||||
|
||||
## `[vfd]`
|
||||
|
||||
Controls emulation of the VFD GP1232A02A FUTABA assembly.
|
||||
@ -636,3 +648,19 @@ Default: Empty string
|
||||
Configure the location of the "Option" data mount point. This mount point is
|
||||
optional (hence the name, probably) and contains directories which contain
|
||||
minor over-the-air content updates.
|
||||
|
||||
## `[epay]`
|
||||
|
||||
Configure Thinca Payment (E-Money) emulation and hooks.
|
||||
|
||||
### `enable`
|
||||
|
||||
Default: `1`
|
||||
|
||||
Enables the Thinca emulation. This will allow you to enable E-Money on compatible servers.
|
||||
|
||||
### `hook`
|
||||
|
||||
Default: `0`
|
||||
|
||||
Enables hooking of respective Thinca DLL functions to emulate the existence of E-Money. This cannot be used with a real E-Money server.
|
213
doc/emoney.md
Normal file
213
doc/emoney.md
Normal file
@ -0,0 +1,213 @@
|
||||
# E-Money Authentication Procedure
|
||||
by Haruka Akechi
|
||||
|
||||
### SETTING UP
|
||||
|
||||
1) Obtain the 64 byte long authentication card encryption key. `amdaemon.exe` holds the secrets.
|
||||
|
||||
2) Inside the `emoney\` folder, install the python modules and launch the generator script:
|
||||
|
||||
```shell
|
||||
python -m pip install -r requirements.txt
|
||||
python authcardgen.py --key <ENTER YOUR KEY HERE>
|
||||
```
|
||||
|
||||
```
|
||||
Usage: authcardgen.py [OPTIONS]
|
||||
|
||||
Options:
|
||||
--cardid TEXT Card ID (64 hex characters)
|
||||
--key TEXT Key (128 hex characters, required) [required]
|
||||
--store-card-id TEXT Store Card ID (padded to 16 bytes)
|
||||
--merchant-code TEXT Merchant Code (padded to 20 bytes)
|
||||
--store-branch-id TEXT Store Branch ID (padded to 12 bytes)
|
||||
--passphrase TEXT Passphrase, used for the pfx password (padded to 16 bytes)
|
||||
--output TEXT Output filename
|
||||
--help Show this message and exit.
|
||||
```
|
||||
|
||||
3) Place the generated `authcard.bin` in your `DEVICE\` folder.
|
||||
|
||||
4) Check `tfps-res-pro\env.json` for your game. If it contains a `"use_proxy": true` statement, add to segatools.ini:
|
||||
|
||||
```ini
|
||||
[aime]
|
||||
proxy_flag=3
|
||||
```
|
||||
|
||||
5) Replace the two URLs in `tfps-res-pro\resource.xml` to your servers'. This is to ensure the Host header will match the certificate's.
|
||||
|
||||
6) Where amdaemon.exe is located, there should be a `ca.pem`. Replace this file with either [this](https://curl.se/ca/cacert.pem) for the most common CA's (including Let's Encrypt), or whatever CA the server is using (your server will provide this).
|
||||
|
||||
7) Run your game and enter the test menu, and navigate to E-Money Settings.
|
||||
|
||||
8) Select "Terminal Authentication"
|
||||
|
||||
9) Hold your key for scanning a card (default: RETURN)
|
||||
|
||||
10) If your shop name shows up, everything was done succesfully. Otherwise, check the VFD.
|
||||
|
||||
### TECHNICAL INFO
|
||||
|
||||
For debugging anything e-money related, I highly recommend setting "emoney.log.level" to 4 in your game's amdaemon config.json. This should create a `<amfs>\emoney_log\thincapayment.log`.
|
||||
|
||||
When terminal authentication is started from the test menu, the game will check for an Aime reader of at least generation 3. If that is not fulfilled, the VFD will display "unsupported card reader" and abort. If the card reader is good, the VFD will prompt for a card to be touched, and the reader will start scanning for a MIFARE card, which from this point we call "Thinca Authentication Card". This card contains one unencrypted block (3) which contains:
|
||||
|
||||
[0] = 0x54 // T
|
||||
|
||||
[1] = 0x43 // C
|
||||
|
||||
[2] = proxy_type
|
||||
|
||||
[3] = 0x01
|
||||
|
||||
Afterwards, a number of encrypted blocks are read, namely the blocks 5,6,8,9,10,12,13 and 14. These blocks together form a 130 byte long binary blob that contains the authentication data.
|
||||
|
||||
This data is encrypted as following:
|
||||
|
||||
Given a fixed 0x40 byte long encryption key and a fixed 0x20 byte long static "card ID", both of which can be found in amdaemon:
|
||||
XOR every byte of the encryption key with 0x1C.
|
||||
Calculate a 0x20 bytes long HMAC-SHA-256 of the card ID with the XOR'ed encryption key as the key.
|
||||
Calculate the needed IV by XORing the lower 0x10 bytes of the HMAC with the upper 0x10 bytes of the HMAC:
|
||||
|
||||
```
|
||||
byte[] iv = new byte[16];
|
||||
for (int i = 0; i < 16; i++) {
|
||||
iv[i] = (byte) (hmac[i + 16] ^ hmac[i]);
|
||||
}
|
||||
```
|
||||
|
||||
With this IV, and the HMAC as the key, finally encrypt the data with AES/CBC/PKCS5Padding.
|
||||
|
||||
Now what is actually stored on such a card? This:
|
||||
|
||||
```
|
||||
+---------------+---------------+-----------------+------------+----------+
|
||||
| Store Card ID | Merchant Code | Store Branch ID | Passphrase | NULL |
|
||||
+---------------+---------------+-----------------+------------+----------+
|
||||
| 0x10 bytes | 0x14 bytes | 0xC bytes | 0x10 bytes | 0x1 byte |
|
||||
| char* | char* | uint128_t | char* | NULL |
|
||||
+---------------+---------------+-----------------+------------+----------+
|
||||
```
|
||||
|
||||
Only two things really matter here. The Store Branch ID must be non-zero, otherwise amdaemon will reject it, and the passphrase, which is the PFX key password (passphrase during authcard creation) for the certificate returned in the network response (see below).
|
||||
|
||||
Technically with the Store Card ID you could bind different auth cards to different users, but for home usage, it really doesn't matter.
|
||||
|
||||
That's the Thinca Authentication Card out of the way, so continue on to:
|
||||
|
||||
### NETWORK
|
||||
|
||||
First, a regular HTTP(S) connection will be made to the URL specified in tfps-res-pro\env.json, tasms.root_endpoint.
|
||||
|
||||
Request Data:
|
||||
`{"modelName":"ACA","serialNumber":"ACAE01A9999","merchantCode":"NOTSEGA","storeBranchNumber":11111,"storeCardId":"FAKESTORE"}`
|
||||
|
||||
Note that the serialNumber here actually isn't the keychip ID, but rather the PCBID. The three other values are read from the Thinca Authentication Card.
|
||||
|
||||
Response Headers:
|
||||
`x-certificate-md5: <md5 of the certificate string>`
|
||||
|
||||
Response Data:
|
||||
```
|
||||
{
|
||||
"certificate": "<base 64 encoded pfx string, with the authentication card's passphrase being the pfx password>",
|
||||
"initSettings": {
|
||||
"endpoints": {
|
||||
"terminals": {
|
||||
"uri": "https://localhost/emoney/terminals"
|
||||
},
|
||||
"statuses": {
|
||||
"uri": "https://localhost/emoney/statuses"
|
||||
},
|
||||
"sales": {
|
||||
"uri": "https://localhost/emoney/sales"
|
||||
},
|
||||
"counters": {
|
||||
"uri": "https://localhost/emoney/counters"
|
||||
}
|
||||
},
|
||||
"intervals": {
|
||||
"checkSetting": 60,
|
||||
"sendStatus": 60
|
||||
},
|
||||
"settigsType": "AmusementTerminalSettings", // sic
|
||||
"status": "1", // a string
|
||||
"terminalId": "536453645364536453645364536453", // must be exactly 30 characters
|
||||
"version": "2024-01-01T01:01:01", // a timestamp
|
||||
"availableElectronicMoney": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
5,
|
||||
6,
|
||||
8,
|
||||
9,
|
||||
91, // aimepay
|
||||
101 // "cash" ???
|
||||
],
|
||||
"cashAvailability": true,
|
||||
"productCode": 1337
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Next up, we will connect to the TLAM service to get the URL for the TCAP service. Everything from this point on requires not only HTTPS, but also client certificate validation (which is the certificate returned from the previous request). Technically you don't need to validate it, but you must accept a client certificate or the client HTTPS library will not be happy.
|
||||
|
||||
The client certificate itself must be signed with the same key than the server's HTTPS certificate.
|
||||
|
||||
The client AND server certificate must have it's CA included in the "ca.pem" file in amdaemon's directory. You can freely replace this file with https://curl.se/ca/cacert.pem to allow Let's Encrypt and whatever else.
|
||||
|
||||
At this point, "ThincaPayment::setClientCertificate(). ErrCode 203" means that the downloaded file couldn't be found, or the certificate password is wrong.
|
||||
|
||||
[Warn ] TCAP communicate error 06514086 means the ca.pem has no entry for the given server CA.
|
||||
|
||||
TLAM:
|
||||
The TLAM url comes from tftp-res-pro\resource.xml, commonPrimaryUri.
|
||||
|
||||
<commonPrimaryUri>/initauth.jsp
|
||||
|
||||
Request Data:
|
||||
<none>
|
||||
Response Headers:
|
||||
`Content-Type: application/x-tlam`
|
||||
Response Data:
|
||||
`SERV=https://localhost/emoney/tcap`
|
||||
|
||||
TCAP:
|
||||
Here be dragons.
|
||||
|
||||
Is it this?? https://en.wikipedia.org/wiki/Transaction_Capabilities_Application_Part
|
||||
Maybe thincatcapclient.dll holds the secrets?
|
||||
|
||||
Request Data:
|
||||
```
|
||||
02 05 01 00 ba 00 00 00 21 00 00 00 00 00 25 00 ....º... !.....%.
|
||||
9f 00 01 00 00 07 47 65 6e 65 72 69 63 06 43 4c ......Ge neric.CL
|
||||
49 45 4e 54 00 02 00 00 07 47 65 6e 65 72 69 63 IENT.... .Generic
|
||||
06 53 54 41 54 55 53 00 03 00 00 07 47 65 6e 65 .STATUS. ....Gene
|
||||
72 69 63 06 4f 50 54 49 4f 4e 00 04 00 00 06 46 ric.OPTI ON.....F
|
||||
65 6c 69 43 61 03 52 2f 57 00 05 00 00 07 47 65 eliCa.R/ W.....Ge
|
||||
6e 65 72 69 63 09 52 2f 57 5f 45 56 45 4e 54 00 neric.R/ W_EVENT.
|
||||
06 00 00 07 47 65 6e 65 72 69 63 0a 52 2f 57 5f ....Gene ric.R/W_
|
||||
53 54 41 54 55 53 00 07 00 00 07 47 65 6e 65 72 STATUS.. ...Gener
|
||||
69 63 0a 52 2f 57 5f 4f 50 54 49 4f 4e 00 08 00 ic.R/W_O PTION...
|
||||
00 07 47 65 6e 65 72 69 63 06 4e 46 43 5f 52 57 ..Generi c.NFC_RW
|
||||
00 00 00 26 00 03 02 05 00 00 00 00 22 00 00 ...&.... ...."..
|
||||
```
|
||||
|
||||
and
|
||||
```
|
||||
02 05 03 00 17 00 00 00 21 00 11 34 34 20 30 30 ........ !..44 00
|
||||
20 30 31 20 32 31 20 30 30 20 30 30 01 21 0 0 00
|
||||
```
|
||||
|
||||
Response Headers:
|
||||
```
|
||||
Content-Type: application/x-tcap
|
||||
Transfer-Encoding: chunked
|
||||
```
|
||||
Response Data:
|
||||
????
|
||||
|
||||
To be continued ...
|
131
emoney/authcardgen.py
Normal file
131
emoney/authcardgen.py
Normal file
@ -0,0 +1,131 @@
|
||||
import hmac
|
||||
import hashlib
|
||||
import click
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad, unpad
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class CardEncryptor:
|
||||
def __init__(
|
||||
self,
|
||||
cardid_hex,
|
||||
key_hex,
|
||||
store_card_id_str="FAKESTORE",
|
||||
merchant_code_str="NOTSEGA",
|
||||
store_branch_id_str="11111",
|
||||
passphrase_str="573",
|
||||
output_file="authdata.bin",
|
||||
):
|
||||
self.cardid = bytes.fromhex(cardid_hex)
|
||||
self.key = bytearray.fromhex(key_hex)
|
||||
self.output_file = output_file
|
||||
|
||||
if len(self.cardid) != 0x20:
|
||||
raise ValueError("Card ID must be 32 bytes (64 hex characters)")
|
||||
if len(self.key) != 0x40:
|
||||
raise ValueError("Key must be 64 bytes (128 hex characters)")
|
||||
|
||||
# XOR the key with 0x1C as in original Java
|
||||
for i in range(len(self.key)):
|
||||
self.key[i] ^= 0x1C
|
||||
|
||||
self.store_card_id = self._str_to_bytes(store_card_id_str, 0x10)
|
||||
self.merchant_code = self._str_to_bytes(merchant_code_str, 0x14)
|
||||
self.store_branch_id = self._str_to_bytes(store_branch_id_str, 0x0C)
|
||||
self.passphrase = self._str_to_bytes(passphrase_str, 0x10)
|
||||
|
||||
# Construct full data payload
|
||||
self.data = (
|
||||
self.store_card_id
|
||||
+ self.merchant_code
|
||||
+ self.store_branch_id
|
||||
+ self.passphrase
|
||||
+ bytes([0x00]) # +1 null terminator / padding byte
|
||||
)
|
||||
|
||||
def _str_to_bytes(self, s, length):
|
||||
b = bytearray(length)
|
||||
b[: len(s)] = s.encode()
|
||||
return bytes(b)
|
||||
|
||||
def _bytes_to_str(self, b):
|
||||
return b.decode()
|
||||
|
||||
def _bytes_to_hex(self, b):
|
||||
return b.hex().upper()
|
||||
|
||||
def _calculate_hmac_sha256(self, key, data, length):
|
||||
h = hmac.new(key, data, hashlib.sha256)
|
||||
return h.digest()[:length]
|
||||
|
||||
def _aes_cbc_encrypt(self, key, data, iv):
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
return cipher.encrypt(pad(data, AES.block_size))
|
||||
|
||||
def _aes_cbc_decrypt(self, key, data, iv):
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
return unpad(cipher.decrypt(data), AES.block_size)
|
||||
|
||||
def run(self):
|
||||
print("Card ID:\t\t", self._bytes_to_hex(self.cardid))
|
||||
print("Store Card ID:\t\t", self._bytes_to_str(self.store_card_id))
|
||||
print("Merchant Code:\t\t", self._bytes_to_str(self.merchant_code))
|
||||
print("Store Branch ID:\t", self._bytes_to_str(self.store_branch_id))
|
||||
print("Passphrase:\t\t", self._bytes_to_str(self.passphrase))
|
||||
|
||||
hmac_output = self._calculate_hmac_sha256(self.key, self.cardid, 0x20)
|
||||
# print("HMAC:\t\t", self._bytes_to_hex(hmac_output))
|
||||
|
||||
iv = bytes([hmac_output[i + 16] ^ hmac_output[i] for i in range(16)])
|
||||
# print("IV:\t\t", self._bytes_to_hex(iv))
|
||||
|
||||
encrypted = self._aes_cbc_encrypt(hmac_output, self.data, iv)
|
||||
# print("ENCRYPTED:\t", self._bytes_to_hex(encrypted))
|
||||
Path(self.output_file).write_bytes(encrypted)
|
||||
|
||||
decrypted = self._aes_cbc_decrypt(hmac_output, encrypted, iv)
|
||||
# print("DECRYPTED:\t", self._bytes_to_hex(decrypted))
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--cardid",
|
||||
default="0102030401020304010203040102030401020304010203040102030401020304",
|
||||
help="Card ID (64 hex characters)",
|
||||
)
|
||||
@click.option(
|
||||
"--key",
|
||||
required=True,
|
||||
help="Key (128 hex characters, required)",
|
||||
)
|
||||
@click.option(
|
||||
"--store-card-id", default="FAKESTORE", help="Store Card ID (padded to 16 bytes)"
|
||||
)
|
||||
@click.option(
|
||||
"--merchant-code", default="NOTSEGA", help="Merchant Code (padded to 20 bytes)"
|
||||
)
|
||||
@click.option(
|
||||
"--store-branch-id", default="11111", help="Store Branch ID (padded to 12 bytes)"
|
||||
)
|
||||
@click.option("--passphrase", default="573", help="Passphrase, used for the pfx password (padded to 16 bytes)")
|
||||
@click.option("--output", default="authdata.bin", help="Output filename")
|
||||
def cli(cardid, key, store_card_id, merchant_code, store_branch_id, passphrase, output):
|
||||
if len(key) != 128 or not all(c in "0123456789abcdefABCDEF" for c in key):
|
||||
raise click.BadParameter("The key must be a 128-character hexadecimal string.")
|
||||
|
||||
encryptor = CardEncryptor(
|
||||
cardid,
|
||||
key,
|
||||
store_card_id_str=store_card_id,
|
||||
merchant_code_str=merchant_code,
|
||||
store_branch_id_str=store_branch_id,
|
||||
passphrase_str=passphrase,
|
||||
output_file=output,
|
||||
)
|
||||
encryptor.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
2
emoney/requirements.txt
Normal file
2
emoney/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
click
|
||||
pycryptodome
|
@ -217,20 +217,24 @@ static void dns_hook_init(void)
|
||||
dns_hook_initted = true;
|
||||
InitializeCriticalSection(&dns_hook_lock);
|
||||
|
||||
dns_hook_apply_hooks(NULL);
|
||||
}
|
||||
|
||||
void dns_hook_apply_hooks(HMODULE mod){
|
||||
hook_table_apply(
|
||||
NULL,
|
||||
mod,
|
||||
"dnsapi.dll",
|
||||
dns_hook_syms_dnsapi,
|
||||
_countof(dns_hook_syms_dnsapi));
|
||||
|
||||
hook_table_apply(
|
||||
NULL,
|
||||
mod,
|
||||
"ws2_32.dll",
|
||||
dns_hook_syms_ws2,
|
||||
_countof(dns_hook_syms_ws2));
|
||||
|
||||
hook_table_apply(
|
||||
NULL,
|
||||
mod,
|
||||
"winhttp.dll",
|
||||
dns_hook_syms_winhttp,
|
||||
_countof(dns_hook_syms_winhttp));
|
||||
|
@ -8,3 +8,4 @@ void port_hook_init(unsigned short _startup_port, unsigned short _billing_port,
|
||||
// if to_src is NULL, all lookups for from_src will fail
|
||||
HRESULT dns_hook_push(const wchar_t *from_src, const wchar_t *to_src);
|
||||
|
||||
void dns_hook_apply_hooks(HMODULE mod);
|
||||
|
@ -11,6 +11,8 @@
|
||||
|
||||
#include "hooklib/path.h"
|
||||
|
||||
#include <util/dprintf.h>
|
||||
|
||||
/* Helpers */
|
||||
|
||||
static void path_hook_init(void);
|
||||
@ -533,6 +535,12 @@ static BOOL path_transform_w(wchar_t **out, const wchar_t *src)
|
||||
goto end;
|
||||
}
|
||||
|
||||
#if LOG_VFS
|
||||
if (!wcsstr(src, L"AppUser")) {
|
||||
dprintf("Path: %ls -> %ls\n", src, dest);
|
||||
}
|
||||
#endif
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -82,6 +82,9 @@ endif
|
||||
if get_option('log_all') or get_option('log_clock')
|
||||
add_project_arguments('-DLOG_CLOCK', language: 'c')
|
||||
endif
|
||||
if get_option('log_all') or get_option('log_vfs')
|
||||
add_project_arguments('-DLOG_VFS', language: 'c')
|
||||
endif
|
||||
|
||||
shlwapi_lib = cc.find_library('shlwapi')
|
||||
dinput8_lib = cc.find_library('dinput8')
|
||||
|
@ -73,3 +73,8 @@ option('log_clock',
|
||||
value : false,
|
||||
description : 'Enable debug logging for clock APIs'
|
||||
)
|
||||
option('log_vfs',
|
||||
type : 'boolean',
|
||||
value : false,
|
||||
description : 'Enable debug logging for file system redirections'
|
||||
)
|
||||
|
@ -360,4 +360,5 @@ void epay_config_load(struct epay_config *cfg, const wchar_t *filename)
|
||||
assert(filename != NULL);
|
||||
|
||||
cfg->enable = GetPrivateProfileIntW(L"epay", L"enable", 1, filename);
|
||||
cfg->hook = GetPrivateProfileIntW(L"epay", L"hook", 0, filename);
|
||||
}
|
||||
|
@ -7,10 +7,13 @@
|
||||
|
||||
#include "hook/table.h"
|
||||
|
||||
#include "hooklib/dns.h"
|
||||
#include "hooklib/reg.h"
|
||||
|
||||
#include "platform/epay.h"
|
||||
|
||||
#include <hooklib/path.h>
|
||||
|
||||
#include "util/dprintf.h"
|
||||
|
||||
static HRESULT misc_read_thinca_adapter(void *bytes, uint32_t *nbytes);
|
||||
@ -119,15 +122,8 @@ static const struct hook_symbol epay_syms[] = {
|
||||
}
|
||||
};
|
||||
|
||||
HRESULT epay_hook_init(const struct epay_config *cfg) {
|
||||
HRESULT hr;
|
||||
assert(cfg != NULL);
|
||||
|
||||
if (!cfg->enable) {
|
||||
return S_FALSE;
|
||||
}
|
||||
|
||||
hr = reg_hook_push_key(
|
||||
HRESULT epay_apply_registry_hooks(){
|
||||
HRESULT hr = reg_hook_push_key(
|
||||
HKEY_LOCAL_MACHINE,
|
||||
L"SOFTWARE\\TFPaymentService\\ThincaRwAdapter",
|
||||
epay_adapter_keys,
|
||||
@ -163,6 +159,40 @@ HRESULT epay_hook_init(const struct epay_config *cfg) {
|
||||
epay_tcap_url1_keys,
|
||||
_countof(epay_tcap_url1_keys));
|
||||
|
||||
return hr;
|
||||
}
|
||||
|
||||
HRESULT epay_hook_init(const struct epay_config *cfg) {
|
||||
HRESULT hr;
|
||||
assert(cfg != NULL);
|
||||
|
||||
if (!cfg->enable) {
|
||||
return S_FALSE;
|
||||
}
|
||||
|
||||
hr = epay_apply_registry_hooks();
|
||||
if (FAILED(hr)){
|
||||
return hr;
|
||||
}
|
||||
|
||||
dprintf("EPay: Registry initialized\n");
|
||||
|
||||
// HACK:(?) the DLLs are loaded dynamically so we just preload it and apply DNS and VFS hooks to it
|
||||
HMODULE thincahttpclient = LoadLibraryA("thincahttpclient.dll");
|
||||
if (thincahttpclient != NULL){
|
||||
dns_hook_apply_hooks(thincahttpclient);
|
||||
path_hook_insert_hooks(thincahttpclient);
|
||||
}
|
||||
HMODULE thincapayment = LoadLibraryA("ThincaPayment.dll");
|
||||
if (thincapayment != NULL){
|
||||
path_hook_insert_hooks(thincapayment);
|
||||
}
|
||||
HMODULE thincatcapclient = LoadLibraryA("thincatcapclient.dll");
|
||||
if (thincatcapclient != NULL){
|
||||
path_hook_insert_hooks(thincatcapclient);
|
||||
}
|
||||
|
||||
if (cfg->hook) {
|
||||
hook_table_apply(
|
||||
NULL,
|
||||
"ThincaPayment.dll",
|
||||
@ -189,7 +219,8 @@ HRESULT epay_hook_init(const struct epay_config *cfg) {
|
||||
thinca_stub->impl1->unk220 = thinca_unk;
|
||||
thinca_stub->impl1->unk228 = thinca_unk;
|
||||
|
||||
dprintf("Epay: Init.\n");
|
||||
dprintf("Epay: Hooks initialized\n");
|
||||
}
|
||||
|
||||
return hr;
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
#pragma pack(push,1)
|
||||
struct epay_config {
|
||||
bool enable;
|
||||
bool hook;
|
||||
};
|
||||
|
||||
/* The functions in these structs are how clients like amdaemon interface
|
||||
@ -62,3 +63,4 @@ struct thinca_main {
|
||||
|
||||
#pragma pack(pop)
|
||||
HRESULT epay_hook_init(const struct epay_config *cfg);
|
||||
HRESULT epay_apply_registry_hooks();
|
@ -21,6 +21,8 @@ util_lib = static_library(
|
||||
'get_function_ordinal.h',
|
||||
'lib.c',
|
||||
'lib.h',
|
||||
'slurp.c',
|
||||
'slurp.h',
|
||||
'str.c',
|
||||
'str.h',
|
||||
'env.c',
|
||||
|
67
util/slurp.c
Normal file
67
util/slurp.c
Normal file
@ -0,0 +1,67 @@
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
/*
|
||||
* 'slurp' reads the file identified by 'path' into a character buffer
|
||||
* pointed at by 'buf', optionally adding a terminating NUL if
|
||||
* 'add_nul' is true. On success, the size of the file is returned; on
|
||||
* failure, -1 is returned and ERRNO is set by the underlying system
|
||||
* or library call that failed.
|
||||
*
|
||||
* WARNING: 'slurp' malloc()s memory to '*buf' which must be freed by
|
||||
* the caller.
|
||||
*/
|
||||
long wslurp(const wchar_t* path, char **buf, bool add_nul)
|
||||
{
|
||||
FILE *fp;
|
||||
size_t fsz;
|
||||
long off_end;
|
||||
int rc;
|
||||
|
||||
/* Open the file */
|
||||
fp = _wfopen(path, L"rb");
|
||||
if( NULL == fp ) {
|
||||
return -1L;
|
||||
}
|
||||
|
||||
/* Seek to the end of the file */
|
||||
rc = fseek(fp, 0L, SEEK_END);
|
||||
if( 0 != rc ) {
|
||||
return -1L;
|
||||
}
|
||||
|
||||
/* Byte offset to the end of the file (size) */
|
||||
if( 0 > (off_end = ftell(fp)) ) {
|
||||
return -1L;
|
||||
}
|
||||
fsz = (size_t)off_end;
|
||||
|
||||
/* Allocate a buffer to hold the whole file */
|
||||
*buf = malloc( fsz+(int)add_nul );
|
||||
if( NULL == *buf ) {
|
||||
return -1L;
|
||||
}
|
||||
|
||||
/* Rewind file pointer to start of file */
|
||||
rewind(fp);
|
||||
|
||||
/* Slurp file into buffer */
|
||||
if( fsz != fread(*buf, 1, fsz, fp) ) {
|
||||
free(*buf);
|
||||
return -1L;
|
||||
}
|
||||
|
||||
/* Close the file */
|
||||
if( EOF == fclose(fp) ) {
|
||||
free(*buf);
|
||||
return -1L;
|
||||
}
|
||||
|
||||
if( add_nul ) {
|
||||
/* Make sure the buffer is NUL-terminated, just in case */
|
||||
buf[fsz] = '\0';
|
||||
}
|
||||
|
||||
/* Return the file size */
|
||||
return (long)fsz;
|
||||
}
|
12
util/slurp.h
Normal file
12
util/slurp.h
Normal file
@ -0,0 +1,12 @@
|
||||
#include <stdbool.h>
|
||||
/*
|
||||
* 'slurp' reads the file identified by 'path' into a character buffer
|
||||
* pointed at by 'buf', optionally adding a terminating NUL if
|
||||
* 'add_nul' is true. On success, the size of the file is returned; on
|
||||
* failure, -1 is returned and ERRNO is set by the underlying system
|
||||
* or library call that failed.
|
||||
*
|
||||
* WARNING: 'slurp' malloc()s memory to '*buf' which must be freed by
|
||||
* the caller.
|
||||
*/
|
||||
long wslurp(const wchar_t* path, char **buf, bool add_nul);
|
Reference in New Issue
Block a user