segatools/emoney/authcardgen.py

132 lines
4.4 KiB
Python

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