3 Commits

Author SHA1 Message Date
243d40cfd8 Add changelog and docs 2024-06-18 09:39:14 +07:00
bf8631448a support SDGS encryption 2024-06-15 22:22:07 +07:00
c3efc36be2 [chuni] Add correct endpoint iter_counts for all versions with encryption 2024-06-12 16:09:36 +07:00
3 changed files with 67 additions and 20 deletions

View File

@ -1,6 +1,10 @@
# Changelog # Changelog
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
## 20240616
### CHUNITHM
+ Support network encryption for Export/International versions
## 20240530 ## 20240530
### DIVA ### DIVA
+ Fix reader for when dificulty is not a int + Fix reader for when dificulty is not a int

View File

@ -87,13 +87,19 @@ Config file is located in `config/chuni.yaml`.
| `crypto` | This option is used to enable the TLS Encryption | | `crypto` | This option is used to enable the TLS Encryption |
**If you would like to use network encryption, the following will be required underneath but key, iv and hash are required:** If you would like to use network encryption, add the keys to the `keys` section under `crypto`, where the key
is the version ID for Japanese (SDHD) versions and `"{versionID}_int"` for Export (SDGS) versions, and the value
is an array containing `[key, iv, salt, iter_count]` in order.
`iter_count` is optional for all Japanese (SDHD) versions but may be required for some Export (SDGS) versions.
You will receive an error in the logs if it needs to be specified.
```yaml ```yaml
crypto: crypto:
encrypted_only: False encrypted_only: False
keys: keys:
13: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] 13: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"]
"13_int": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000", 42]
``` ```
### Database upgrade ### Database upgrade

View File

@ -39,7 +39,7 @@ class ChuniServlet(BaseServlet):
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
super().__init__(core_cfg, cfg_dir) super().__init__(core_cfg, cfg_dir)
self.game_cfg = ChuniConfig() self.game_cfg = ChuniConfig()
self.hash_table: Dict[Dict[str, str]] = {} self.hash_table: Dict[str, Dict[str, str]] = {}
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"): if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
self.game_cfg.update( self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}")) yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"))
@ -89,30 +89,59 @@ class ChuniServlet(BaseServlet):
) )
self.logger.inited = True self.logger.inited = True
known_iter_counts = {
ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS: 67,
ChuniConstants.VER_CHUNITHM_PARADISE: 44,
f"{ChuniConstants.VER_CHUNITHM_PARADISE}_int": 25,
ChuniConstants.VER_CHUNITHM_NEW: 54,
f"{ChuniConstants.VER_CHUNITHM_NEW}_int": 49,
ChuniConstants.VER_CHUNITHM_NEW_PLUS: 25,
ChuniConstants.VER_CHUNITHM_SUN: 70,
ChuniConstants.VER_CHUNITHM_SUN_PLUS: 36,
}
for version, keys in self.game_cfg.crypto.keys.items(): for version, keys in self.game_cfg.crypto.keys.items():
if len(keys) < 3: if len(keys) < 3:
continue continue
self.hash_table[version] = {} if isinstance(version, int):
version_idx = version
else:
version_idx = int(version.split("_")[0])
salt = bytes.fromhex(keys[2])
if len(keys) >= 4:
iter_count = keys[3]
elif (iter_count := known_iter_counts.get(version)) is None:
self.logger.error(
"Number of iteration rounds for version %s is not known, but it is not specified in the config",
version,
)
continue
self.hash_table[version] = {}
method_list = [ method_list = [
method method
for method in dir(self.versions[version]) for method in dir(self.versions[version_idx])
if not method.startswith("__") if not method.startswith("__")
] ]
for method in method_list: for method in method_list:
method_fixed = inflection.camelize(method)[6:-7] method_fixed = inflection.camelize(method)[6:-7]
# number of iterations was changed to 70 in SUN and then to 36
if version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: # This only applies for CHUNITHM NEW International and later for some reason.
iter_count = 36 # CHUNITHM SUPERSTAR (PLUS) did not add "Exp" to the endpoint when hashing.
elif version == ChuniConstants.VER_CHUNITHM_SUN: if (
iter_count = 70 isinstance(version, str)
else: and version.endswith("_int")
iter_count = 44 and version_idx >= ChuniConstants.VER_CHUNITHM_NEW
):
method_fixed += "C3Exp"
hash = PBKDF2( hash = PBKDF2(
method_fixed, method_fixed,
bytes.fromhex(keys[2]), salt,
128, 128,
count=iter_count, count=iter_count,
hmac_hash_module=SHA1, hmac_hash_module=SHA1,
@ -122,7 +151,7 @@ class ChuniServlet(BaseServlet):
self.hash_table[version][hashed_name] = method_fixed self.hash_table[version][hashed_name] = method_fixed
self.logger.debug( self.logger.debug(
f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}" f"Hashed v{version} method {method_fixed} with {salt} to get {hashed_name}"
) )
@classmethod @classmethod
@ -211,31 +240,39 @@ class ChuniServlet(BaseServlet):
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
# If we get a 32 character long hex string, it's a hash and we're # If we get a 32 character long hex string, it's a hash and we're
# doing encrypted. The likelyhood of false positives is low but # doing encrypted. The likelihood of false positives is low but
# technically not 0 # technically not 0
if game_code == "SDGS":
crypto_cfg_key = f"{internal_ver}_int"
hash_table_key = f"{internal_ver}_int"
else:
crypto_cfg_key = internal_ver
hash_table_key = internal_ver
if internal_ver < ChuniConstants.VER_CHUNITHM_NEW: if internal_ver < ChuniConstants.VER_CHUNITHM_NEW:
endpoint = request.headers.get("User-Agent").split("#")[0] endpoint = request.headers.get("User-Agent").split("#")[0]
else: else:
if internal_ver not in self.hash_table: if hash_table_key not in self.hash_table:
self.logger.error( self.logger.error(
f"v{version} does not support encryption or no keys entered" f"v{version} does not support encryption or no keys entered"
) )
return Response(zlib.compress(b'{"stat": "0"}')) return Response(zlib.compress(b'{"stat": "0"}'))
elif endpoint.lower() not in self.hash_table[internal_ver]: elif endpoint.lower() not in self.hash_table[hash_table_key]:
self.logger.error( self.logger.error(
f"No hash found for v{version} endpoint {endpoint}" f"No hash found for v{version} endpoint {endpoint}"
) )
return Response(zlib.compress(b'{"stat": "0"}')) return Response(zlib.compress(b'{"stat": "0"}'))
endpoint = self.hash_table[internal_ver][endpoint.lower()] endpoint = self.hash_table[hash_table_key][endpoint.lower()]
try: try:
crypt = AES.new( crypt = AES.new(
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][0]),
AES.MODE_CBC, AES.MODE_CBC,
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][1]),
) )
req_raw = crypt.decrypt(req_raw) req_raw = crypt.decrypt(req_raw)