1
0
mirror of synced 2024-09-24 03:18:22 +02:00
bemaniutils/bemani/protocol/protocol.py

294 lines
10 KiB
Python

import binascii
import hashlib
from typing import Optional
from typing_extensions import Final
from bemani.protocol.lz77 import Lz77
from bemani.protocol.binary import BinaryEncoding
from bemani.protocol.xml import XmlEncoding
from bemani.protocol.node import Node
class EAmuseException(Exception):
"""
An exception thrown when we encounter an error with E-Amusement encapsulation.
"""
class EAmuseProtocol:
"""
A wrapper object that encapsulates encoding/decoding the E-Amusement protocol by Konami.
"""
SHARED_SECRET: Final[bytes] = b'\x69\xD7\x46\x27\xD9\x85\xEE\x21\x87\x16\x15\x70\xD0\x8D\x93\xB1\x24\x55\x03\x5B\x6D\xF0\xD8\x20\x5D\xF5'
XML: Final[int] = 1
BINARY: Final[int] = 2
SHIFT_JIS_LEGACY: Final[str] = "shift-jis-legacy"
SHIFT_JIS: Final[str] = "shift-jis"
EUC_JP: Final[str] = "euc-jp"
UTF_8: Final[str] = "utf-8"
ASCII: Final[str] = "ascii"
def __init__(self) -> None:
"""
Initialize the object.
"""
self.last_text_encoding: Optional[str] = None
self.last_packet_encoding: Optional[int] = None
def _rc4_crypt(self, data: bytes, key: bytes) -> bytes:
"""
Given a data blob and a key blob, perform RC4 encryption/decryption.
Parameters:
data - Binary string representing data to be encrypted/decrypted
key - Binary string representing the key to use
Returns:
binary string representing the encrypted/decrypted data
"""
S = list(range(256))
j = 0
out = []
# KSA Phase
for i in range(256):
j = (j + S[i] + key[i % len(key)]) & 0xFF
S[i], S[j] = S[j], S[i]
# PRGA Phase
i = j = 0
for char in data:
i = (i + 1) & 0xFF
j = (j + S[i]) & 0XFF
S[i], S[j] = S[j], S[i]
out.append(char ^ S[(S[i] + S[j]) & 0xFF])
return bytes(out)
def __decrypt(self, encryption_key: Optional[str], data: bytes) -> bytes:
"""
Given data and an optional encryption key, decrypt the data.
Parameters:
encryption_key - A string encryption key as returned from a HTTP request.
Should be in the form 1-xxyyzzww-aabb. If it is None, this
performs a null decrypt.
data - Binary string representing data to transform.
Returns:
binary string representing transformed data
"""
if data is None:
return None
key: Optional[bytes] = None
if encryption_key:
# Key is concatenated with the shared secret above
version, first, second = encryption_key.split('-')
key = binascii.unhexlify((first + second).encode('ascii')) + EAmuseProtocol.SHARED_SECRET
# Next, key is sent through MD5 to derive the real key
m = hashlib.md5()
m.update(key)
key = m.digest()
if key:
# This is an encrypted old-style packet
return self._rc4_crypt(data, key)
# No encryption
return data
def __encrypt(self, encryption_key: Optional[str], data: bytes) -> bytes:
"""
Given data and an optional encryption key, encrypt the data.
Parameters:
encryption_key - A string encryption key as returned from a HTTP request.
Should be in the form 1-xxyyzzww-aabb. If it is None, this
performs a null decrypt.
data - Binary string representing data to transform.
Returns:
binary string representing transformed data
"""
# RC4 is symmetric
return self.__decrypt(encryption_key, data)
def __decompress(self, compression: Optional[str], data: bytes) -> bytes:
"""
Given data and an optional compression scheme, decompress the data.
Parameters:
compression - A string specifying the compression used. Should
be of the form 'l7zz' or 'none'. You can also pass in
the python value None.
data - Binary string representing data to transform.
Returns:
binary string representing transformed data
"""
if data is None:
return None
if compression is None or compression == 'none':
# This isn't compressed
return data
elif compression == 'lz77':
# This is a compressed new-style packet
lz = Lz77()
return lz.decompress(data)
else:
raise EAmuseException(f'Unknown compression {compression}')
def __compress(self, compression: Optional[str], data: bytes) -> bytes:
"""
Given data and an optional compression scheme, compress the data.
Parameters:
compression - A string specifying the compression used. Should
be of the form 'l7zz' or 'none'. The python value
None will also be recognized as 'none'.
data - Binary string representing data to transform.
Returns:
binary string representing transformed data
"""
if data is None:
return None
if compression is None or compression == 'none':
# This isn't compressed
return data
elif compression == 'lz77':
# This is a compressed new-style packet
lz = Lz77()
return lz.compress(data)
else:
raise EAmuseException(f'Unknown compression {compression}')
def __decode(self, data: bytes) -> Node:
"""
Given data, decode the data into a Node tree.
Parameters:
data - Binary string representing data to decode.
Returns:
Node tree on success or None on failure.
"""
# Assume it's a binary page
binary = BinaryEncoding()
ret = binary.decode(data, skip_on_exceptions=True)
if ret is not None:
# We got a result, it was binary
self.last_text_encoding = binary.encoding
self.last_packet_encoding = EAmuseProtocol.BINARY
return ret
# Assume its XML
xml = XmlEncoding()
ret = xml.decode(data, skip_on_exceptions=True)
if ret is not None:
# We got a result, it was XML
self.last_text_encoding = xml.encoding
self.last_packet_encoding = EAmuseProtocol.XML
return ret
# Couldn't decode
raise EAmuseException('Unknown packet encoding')
def __encode(self, tree: Node, text_encoding: str, packet_encoding: int) -> bytes:
"""
Given a Node tree, encode the data into the given packet encoding.
Parameters:
tree - A Node object representing the root of the tree to encode.
text_encoding - The text encoding for any strings that will be encoded.
Should be EAmuseProtocol.SHIFT_JIS, EAmuseProtocol.EUC_JP or
EAmuseProtocol.UTF8.
packet_encoding - The encoding used for the packet. Should be EAmuseProtocol.XML
or EAmuseProtocol.BINARY.
Returns:
A string blob representing the encoded packet.
"""
if packet_encoding == EAmuseProtocol.BINARY:
# It's binary, encode it
binary = BinaryEncoding()
return binary.encode(tree, encoding=text_encoding)
elif packet_encoding == EAmuseProtocol.XML:
# It's XML, encode it
xml = XmlEncoding()
return xml.encode(tree, encoding=text_encoding)
else:
raise EAmuseException(f"Invalid packet encoding {packet_encoding}")
def decode(self, compression: Optional[str], encryption: Optional[str], data: bytes) -> Node:
"""
Given a request with optional compression and encryption set, decrypt,
decompress and decode the data, returning a parsed tree.
Parameters:
compression - A string specifying the compression type, should be 'lz77' or 'none'.
The python value None can also be passed in.
encryption - A string specifying the encryption key, or None if no encryption.
data - A binary string of data to parse.
Returns:
A Node tree structure representing the parsed request, or None on failure.
"""
data = self.__decrypt(encryption, data)
data = self.__decompress(compression, data)
return self.__decode(data)
def encode(
self,
compression: Optional[str],
encryption: Optional[str],
tree: Node,
text_encoding: Optional[str]=None,
packet_encoding: Optional[int]=None,
) -> bytes:
"""
Given a response with optional compression and encryption set, encode, compress
and encrypt the data, returning a binary blob suitable for forwarding on a network.
Parameters:
compression - A string specifying the compression type, should be 'lz77' or 'none'.
The python value None can also be passed in.
encryption - A string specifying the encryption key, or None if no encryption.
data - A binary string of data to parse.
text_encoding - A text encoding to use. If not provided, uses the text encoding of the
last decoded packet. See __encode for values.
packet_encpding - A packet encoding to use. If not provided, uses the packet encoding
of the last decoded packet. See __encode for values.
Returns:
A blob of data representing the encoded packet.
"""
# Either auto-set response based on request, or explicitly override in parameters
if text_encoding is None:
text_encoding = self.last_text_encoding
if text_encoding is None:
raise EAmuseException("Unknown text encoding")
if packet_encoding is None:
packet_encoding = self.last_packet_encoding
if packet_encoding is None:
raise EAmuseException("Unknown packet encoding")
# Clear last packet since we sent a response
self.last_text_encoding = None
self.last_packet_encoding = None
data = self.__encode(tree, text_encoding, packet_encoding)
data = self.__compress(compression, data)
return self.__encrypt(encryption, data)