emoney: improce doc and add python script
parent
67eda7458b
commit
015097972a
|
@ -3,17 +3,41 @@ by Haruka Akechi
|
||||||
|
|
||||||
### SETTING UP
|
### 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.
|
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.
|
Technically with the Store Card ID you could bind different auth cards to different users, but for home usage, it really doesn't matter.
|
||||||
|
|
||||||
|
|
|
@ -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()
|
|
@ -0,0 +1,2 @@
|
||||||
|
click
|
||||||
|
pycryptodome
|
Loading…
Reference in New Issue