emoney: improce doc and add python script
parent
67eda7458b
commit
015097972a
|
@ -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.
|
||||
|
||||
|
|
|
@ -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