chuni: fix encrypted hash, update unlock challenge req

This commit is contained in:
2025-08-19 19:25:33 +02:00
parent 91f06ccfd2
commit 3c7ac3ac58
2 changed files with 112 additions and 91 deletions

View File

@ -1,20 +1,22 @@
from starlette.requests import Request import asyncio
from starlette.routing import Route import re
from starlette.responses import Response
import logging import logging
import coloredlogs import coloredlogs
from logging.handlers import TimedRotatingFileHandler
import zlib import zlib
import yaml import yaml
import json import json
import inflection import inflection
import string import string
from os import path
from typing import Tuple, Dict, List
from logging.handlers import TimedRotatingFileHandler
from starlette.requests import Request
from starlette.routing import Route
from starlette.responses import Response
from Crypto.Cipher import AES from Crypto.Cipher import AES
from Crypto.Util.Padding import pad from Crypto.Util.Padding import pad
from Crypto.Protocol.KDF import PBKDF2 from Crypto.Protocol.KDF import PBKDF2
from Crypto.Hash import SHA1 from Crypto.Hash import SHA1
from os import path
from typing import Tuple, Dict, List
from core import CoreConfig, Utils from core import CoreConfig, Utils
from core.title import BaseServlet from core.title import BaseServlet
@ -39,6 +41,7 @@ from .luminous import ChuniLuminous
from .luminousplus import ChuniLuminousPlus from .luminousplus import ChuniLuminousPlus
from .verse import ChuniVerse from .verse import ChuniVerse
class ChuniServlet(BaseServlet): 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)
@ -98,15 +101,15 @@ class ChuniServlet(BaseServlet):
known_iter_counts = { known_iter_counts = {
ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS: 67, ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS: 67,
f"{ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS}_int": 25, # SUPERSTAR f"{ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS}_int": 25, # SUPERSTAR
ChuniConstants.VER_CHUNITHM_PARADISE: 44, ChuniConstants.VER_CHUNITHM_PARADISE: 44,
f"{ChuniConstants.VER_CHUNITHM_PARADISE}_int": 51, # SUPERSTAR PLUS f"{ChuniConstants.VER_CHUNITHM_PARADISE}_int": 51, # SUPERSTAR PLUS
ChuniConstants.VER_CHUNITHM_NEW: 54, ChuniConstants.VER_CHUNITHM_NEW: 54,
f"{ChuniConstants.VER_CHUNITHM_NEW}_int": 49, f"{ChuniConstants.VER_CHUNITHM_NEW}_int": 49,
f"{ChuniConstants.VER_CHUNITHM_NEW}_chn": 37, f"{ChuniConstants.VER_CHUNITHM_NEW}_chn": 37,
ChuniConstants.VER_CHUNITHM_NEW_PLUS: 25, ChuniConstants.VER_CHUNITHM_NEW_PLUS: 25,
f"{ChuniConstants.VER_CHUNITHM_NEW_PLUS}_int": 31, f"{ChuniConstants.VER_CHUNITHM_NEW_PLUS}_int": 31,
f"{ChuniConstants.VER_CHUNITHM_NEW_PLUS}_chn": 35, # NEW f"{ChuniConstants.VER_CHUNITHM_NEW_PLUS}_chn": 35, # NEW
ChuniConstants.VER_CHUNITHM_SUN: 70, ChuniConstants.VER_CHUNITHM_SUN: 70,
f"{ChuniConstants.VER_CHUNITHM_SUN}_int": 35, f"{ChuniConstants.VER_CHUNITHM_SUN}_int": 35,
ChuniConstants.VER_CHUNITHM_SUN_PLUS: 36, ChuniConstants.VER_CHUNITHM_SUN_PLUS: 36,
@ -126,7 +129,7 @@ class ChuniServlet(BaseServlet):
version_idx = version version_idx = version
else: else:
version_idx = int(version.split("_")[0]) version_idx = int(version.split("_")[0])
salt = bytes.fromhex(keys[2]) salt = bytes.fromhex(keys[2])
if len(keys) >= 4: if len(keys) >= 4:
@ -156,12 +159,9 @@ class ChuniServlet(BaseServlet):
and version_idx >= ChuniConstants.VER_CHUNITHM_NEW and version_idx >= ChuniConstants.VER_CHUNITHM_NEW
): ):
method_fixed += "C3Exp" method_fixed += "C3Exp"
elif ( elif isinstance(version, str) and version.endswith("_chn"):
isinstance(version, str)
and version.endswith("_chn")
):
method_fixed += "Chn" method_fixed += "Chn"
hash = PBKDF2( hash = PBKDF2(
method_fixed, method_fixed,
salt, salt,
@ -170,7 +170,8 @@ class ChuniServlet(BaseServlet):
hmac_hash_module=SHA1, hmac_hash_module=SHA1,
) )
hashed_name = hash.hex()[:32] # truncate unused bytes like the game does # truncate unused bytes like the game does
hashed_name = hash.hex()[:32]
self.hash_table[version][hashed_name] = method_fixed self.hash_table[version][hashed_name] = method_fixed
self.logger.debug( self.logger.debug(
@ -192,7 +193,9 @@ class ChuniServlet(BaseServlet):
return True return True
def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: def get_allnet_info(
self, game_code: str, game_ver: int, keychip: str
) -> Tuple[str, str]:
title_port_int = Utils.get_title_port(self.core_cfg) title_port_int = Utils.get_title_port(self.core_cfg)
title_port_ssl_int = Utils.get_title_port_ssl(self.core_cfg) title_port_ssl_int = Utils.get_title_port_ssl(self.core_cfg)
@ -206,7 +209,7 @@ class ChuniServlet(BaseServlet):
if proto == "https": if proto == "https":
t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 else "" t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 else ""
else: else:
t_port = f":{title_port_int}" if title_port_int != 80 else "" t_port = f":{title_port_int}" if title_port_int != 80 else ""
return ( return (
@ -216,8 +219,16 @@ class ChuniServlet(BaseServlet):
def get_routes(self) -> List[Route]: def get_routes(self) -> List[Route]:
return [ return [
Route("/{game:str}/{version:int}/ChuniServlet/{endpoint:str}", self.render_POST, methods=['POST']), Route(
Route("/{game:str}/{version:int}/ChuniServlet/MatchingServer/{endpoint:str}", self.render_POST, methods=['POST']), "/{game:str}/{version:int}/ChuniServlet/{endpoint:str}",
self.render_POST,
methods=["POST"],
),
Route(
"/{game:str}/{version:int}/ChuniServlet/MatchingServer/{endpoint:str}",
self.render_POST,
methods=["POST"],
),
] ]
async def render_POST(self, request: Request) -> bytes: async def render_POST(self, request: Request) -> bytes:
@ -234,67 +245,71 @@ class ChuniServlet(BaseServlet):
internal_ver = 0 internal_ver = 0
client_ip = Utils.get_ip_addr(request) client_ip = Utils.get_ip_addr(request)
if game_code == "SDHD" or game_code == "SDBT": # JP if game_code == "SDHD" or game_code == "SDBT": # JP
if version < 105: # 1.0 if version < 105: # 1.0
internal_ver = ChuniConstants.VER_CHUNITHM internal_ver = ChuniConstants.VER_CHUNITHM
elif version >= 105 and version < 110: # PLUS elif version >= 105 and version < 110: # PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_PLUS
elif version >= 110 and version < 115: # AIR elif version >= 110 and version < 115: # AIR
internal_ver = ChuniConstants.VER_CHUNITHM_AIR internal_ver = ChuniConstants.VER_CHUNITHM_AIR
elif version >= 115 and version < 120: # AIR PLUS elif version >= 115 and version < 120: # AIR PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS
elif version >= 120 and version < 125: # STAR elif version >= 120 and version < 125: # STAR
internal_ver = ChuniConstants.VER_CHUNITHM_STAR internal_ver = ChuniConstants.VER_CHUNITHM_STAR
elif version >= 125 and version < 130: # STAR PLUS elif version >= 125 and version < 130: # STAR PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS
elif version >= 130 and version < 135: # AMAZON elif version >= 130 and version < 135: # AMAZON
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON
elif version >= 135 and version < 140: # AMAZON PLUS elif version >= 135 and version < 140: # AMAZON PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS
elif version >= 140 and version < 145: # CRYSTAL elif version >= 140 and version < 145: # CRYSTAL
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL
elif version >= 145 and version < 150: # CRYSTAL PLUS elif version >= 145 and version < 150: # CRYSTAL PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
elif version >= 150 and version < 200: # PARADISE elif version >= 150 and version < 200: # PARADISE
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE
elif version >= 200 and version < 205: # NEW!! elif version >= 200 and version < 205: # NEW!!
internal_ver = ChuniConstants.VER_CHUNITHM_NEW internal_ver = ChuniConstants.VER_CHUNITHM_NEW
elif version >= 205 and version < 210: # NEW PLUS!! elif version >= 205 and version < 210: # NEW PLUS!!
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
elif version >= 210 and version < 215: # SUN elif version >= 210 and version < 215: # SUN
internal_ver = ChuniConstants.VER_CHUNITHM_SUN internal_ver = ChuniConstants.VER_CHUNITHM_SUN
elif version >= 215 and version < 220: # SUN PLUS elif version >= 215 and version < 220: # SUN PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
elif version >= 220 and version < 225: # LUMINOUS elif version >= 220 and version < 225: # LUMINOUS
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
elif version >= 225 and version < 230: # LUMINOUS PLUS elif version >= 225 and version < 230: # LUMINOUS PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS
elif version >= 230: # VERSE elif version >= 230: # VERSE
internal_ver = ChuniConstants.VER_CHUNITHM_VERSE internal_ver = ChuniConstants.VER_CHUNITHM_VERSE
elif game_code == "SDGS": # Int elif game_code == "SDGS": # Int
if version < 105: # SUPERSTAR if version < 105: # SUPERSTAR
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
elif version >= 105 and version < 110: # SUPERSTAR PLUS *Cursed but needed due to different encryption key elif (
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE version >= 105 and version < 110
elif version >= 110 and version < 115: # NEW ): # SUPERSTAR PLUS *Cursed but needed due to different encryption key
internal_ver = ChuniConstants.VER_CHUNITHM_NEW internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE
elif version >= 115 and version < 120: # NEW PLUS!! elif version >= 110 and version < 115: # NEW
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_NEW
elif version >= 120 and version < 125: # SUN elif version >= 115 and version < 120: # NEW PLUS!!
internal_ver = ChuniConstants.VER_CHUNITHM_SUN internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
elif version >= 125 and version < 130: # SUN PLUS elif version >= 120 and version < 125: # SUN
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_SUN
elif version >= 130 and version < 135: # LUMINOUS elif version >= 125 and version < 130: # SUN PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
elif version >= 135: # LUMINOUS PLUS elif version >= 130 and version < 135: # LUMINOUS
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
elif game_code == "SDHJ": # Chn elif version >= 135: # LUMINOUS PLUS
if version < 110: # NEW internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_NEW elif game_code == "SDHJ": # Chn
elif version >= 110 and version < 120: # NEW *Cursed but needed due to different encryption key if version < 110: # NEW
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_NEW
elif version >= 120: # LUMINOUS elif (
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS version >= 110 and version < 120
): # NEW *Cursed but needed due to different encryption key
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
elif version >= 120: # LUMINOUS
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
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
@ -381,7 +396,7 @@ class ChuniServlet(BaseServlet):
else: else:
endpoint = endpoint endpoint = endpoint
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" func_to_find = "handle_" + self.strict_underscore(endpoint) + "_request"
handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg)
if not hasattr(handler_cls, func_to_find): if not hasattr(handler_cls, func_to_find):
@ -419,3 +434,9 @@ class ChuniServlet(BaseServlet):
) )
return Response(crypt.encrypt(padded)) return Response(crypt.encrypt(padded))
def strict_underscore(self, name: str) -> str:
# Insert underscores between *all* capital letters
name = re.sub(r"([A-Z])([A-Z])", r"\1_\2", name)
return inflection.underscore(name)

View File

@ -60,8 +60,8 @@ class ChuniVerse(ChuniLuminousPlus):
"length": len(game_course_level_list), "length": len(game_course_level_list),
"gameCourseLevelList": game_course_level_list, "gameCourseLevelList": game_course_level_list,
} }
async def handle_get_game_uc_condition_api_request(self, data: Dict) -> Dict: async def handle_get_game_u_c_condition_api_request(self, data: Dict) -> Dict:
unlock_challenges = await self.data.static.get_unlock_challenges(self.version) unlock_challenges = await self.data.static.get_unlock_challenges(self.version)
game_unlock_challenge_condition_list = [] game_unlock_challenge_condition_list = []
@ -84,8 +84,8 @@ class ChuniVerse(ChuniLuminousPlus):
unlock_condition = conditions.get( unlock_condition = conditions.get(
unlock_challenge_id, unlock_challenge_id,
{ {
"type": 3, # always unlocked "type": MapAreaConditionType.ALWAYS_UNLOCKED.value, # always unlocked
"conditionId": 0, "conditionId": -1,
}, },
) )
@ -114,7 +114,7 @@ class ChuniVerse(ChuniLuminousPlus):
"gameUnlockChallengeConditionList": game_unlock_challenge_condition_list, "gameUnlockChallengeConditionList": game_unlock_challenge_condition_list,
} }
async def handle_get_user_uc_api_request(self, data: Dict) -> Dict: async def handle_get_user_u_c_api_request(self, data: Dict) -> Dict:
user_id = data["userId"] user_id = data["userId"]
user_unlock_challenges = await self.data.item.get_unlock_challenges( user_unlock_challenges = await self.data.item.get_unlock_challenges(
@ -167,7 +167,9 @@ class ChuniVerse(ChuniLuminousPlus):
# try adding recommendations in order of: title → artist → genre # try adding recommendations in order of: title → artist → genre
for field in ("title", "artist", "genre"): for field in ("title", "artist", "genre"):
await self._add_recommendations(field, user_rec_music_set, music_info_list, rec_limit) await self._add_recommendations(
field, user_rec_music_set, music_info_list, rec_limit
)
if len(user_rec_music_set) >= rec_limit: if len(user_rec_music_set) >= rec_limit:
break break
@ -220,9 +222,7 @@ class ChuniVerse(ChuniLuminousPlus):
excluding music IDs already in the user's recent ratings and recommendations. excluding music IDs already in the user's recent ratings and recommendations.
""" """
# Collect all existing songId to exclude from recommendations # Collect all existing songId to exclude from recommendations
existing_music_ids = { existing_music_ids = {info["songId"] for info in music_info_list}
info["songId"] for info in music_info_list
}
for music_info in music_info_list: for music_info in music_info_list:
if len(user_rec_music_set) >= limit: if len(user_rec_music_set) >= limit: