emoney: improce doc and add python script

feature/thinca_auth
Dniel97 2025-04-17 19:17:42 +02:00
parent 67eda7458b
commit 015097972a
No known key found for this signature in database
GPG Key ID: DE105D481972329C
3 changed files with 164 additions and 7 deletions

View File

@ -3,17 +3,41 @@ by Haruka Akechi
### SETTING UP
1) Obtain the 64 byte long authentication card encryption key and the 32 byte long static authentication card ID. amdaemon.exe holds the secrets.
1) Obtain the 64 byte long authentication card encryption key. `amdaemon.exe` holds the secrets.
2) Get this java file, insert the ID and key, probably edit the passphrase and compile+run to generate authcard.bin: https://gist.github.com/akechi-haruka/a506184638e695a04eabe8cb53f62c36
2) Inside the `emoney\` folder, install the python modules and launch the generator script:
3) Place authcard.bin in your DEVICE folder.
```shell
python -m pip install -r requirements.txt
python authcardgen.py --key <ENTER YOUR KEY HERE>
```
4) Check tfps-res-pro\env.json for your game. If it contains a "use_proxy: true" statement, add "proxy_flag=3" under [aime]
```
Usage: authcardgen.py [OPTIONS]
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.
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.
```
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).
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.
@ -66,7 +90,7 @@ Now what is actually stored on such a card? This:
+---------------+---------------+-----------------+------------+----------+
```
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 for the certificate returned in the network response (see below).
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.

View 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()

View File

@ -0,0 +1,2 @@
click
pycryptodome