From fbc021ed3152c5952d2601624bbbcaa8a994fb30 Mon Sep 17 00:00:00 2001 From: MetaMiku Date: Wed, 11 Nov 2026 05:07:41 +0000 Subject: [PATCH] fix: use new CN APIManager keyset for app version 6.4.0+ The CN server now selects the API AES keyset by the X-App-Version header. Clients reporting 6.4.0 or later receive responses encrypted with a new key/IV (extracted from the 6.4.0 global-metadata), so decrypting them with the old keyset yielded empty data and msgpack failed with 'Unpack failed: incomplete input'. - Add get_keyset(region, app_version) that falls back to the legacy keyset for CN versions below 6.4.0 - Use it for request encryption / response decryption in AbCache - Add 'cn-legacy' region to apidecrypt for old dumps Fixes #2 --- sssekai/__main__.py | 4 ++-- sssekai/abcache/__init__.py | 6 +++--- sssekai/crypto/APIManager.py | 28 ++++++++++++++++++++++++++-- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/sssekai/__main__.py b/sssekai/__main__.py index cd60571..3fd9a7f 100644 --- a/sssekai/__main__.py +++ b/sssekai/__main__.py @@ -119,9 +119,9 @@ def gooey_only(**kwargs): apidecrypt_parser.add_argument( "--region", type=str, - help="app region", + help="app region (use `cn-legacy` for CN app versions before 6.4.0)", default="jp", - choices=["jp", "tw", "en", "kr", "cn"], + choices=["jp", "tw", "en", "kr", "cn", "cn-legacy"], ) apidecrypt_parser.set_defaults(func=main_apidecrypt) # abdecrypt diff --git a/sssekai/abcache/__init__.py b/sssekai/abcache/__init__.py index 3df59b2..912c8ff 100644 --- a/sssekai/abcache/__init__.py +++ b/sssekai/abcache/__init__.py @@ -64,7 +64,7 @@ def check_field(key, fields): from sssekai import __version__, __version_tuple__ from sssekai.unity import sssekai_get_unity_version -from sssekai.crypto.APIManager import decrypt, encrypt, SEKAI_APIMANAGER_KEYSETS +from sssekai.crypto.APIManager import decrypt, encrypt, get_keyset @dataclass @@ -565,7 +565,7 @@ def request_packed(self, method: str, url: str, data: dict = None, **kwargs): # But this may change in the future, in which case consult the following issues: # https://github.com/msgpack/msgpack-python/issues/326 data = packb(data, use_single_float=True) - data = encrypt(data, SEKAI_APIMANAGER_KEYSETS[self.config.app_region]) + data = encrypt(data, get_keyset(self.config.app_region, self.config.app_version)) resp = self.request(method=method, url=url, data=data, **kwargs) if 400 <= resp.status_code < 600: logger.error(f"{method} {url} {resp.status_code}") @@ -590,7 +590,7 @@ def response_to_dict(self, resp: Response, **kwargs): Returns: dict: Decrypted and unpacked data """ - data = decrypt(resp.content, SEKAI_APIMANAGER_KEYSETS[self.config.app_region]) + data = decrypt(resp.content, get_keyset(self.config.app_region, self.config.app_version)) data = unpackb(data, **kwargs) return data diff --git a/sssekai/crypto/APIManager.py b/sssekai/crypto/APIManager.py index 3a0bd3d..26cb489 100644 --- a/sssekai/crypto/APIManager.py +++ b/sssekai/crypto/APIManager.py @@ -1,5 +1,5 @@ from Crypto.Cipher import AES -from typing import Tuple +from typing import Optional, Tuple def PKCS7_pad(data: bytes, bs) -> bytes: @@ -30,9 +30,33 @@ def encrypt_aes_cbc(data: bytes, key, iv, pad=PKCS7_pad) -> bytes: "jp": (b"g2fcC0ZczN9MTJ61", b"msx3IV0i9XE5uYZ1"), "tw": (b"g2fcC0ZczN9MTJ61", b"msx3IV0i9XE5uYZ1"), "kr": (b"g2fcC0ZczN9MTJ61", b"msx3IV0i9XE5uYZ1"), - "cn": (b"g2fcC0ZczN9MTJ61", b"msx3IV0i9XE5uYZ1"), + # CN switched to a new keyset since app version 6.4.0. + # The server picks the keyset by the `X-App-Version` request header. + "cn": (b"PFSeUG9v6MpohZKO", b"rJn6VC6vJhEX2xLO"), + "cn-legacy": (b"g2fcC0ZczN9MTJ61", b"msx3IV0i9XE5uYZ1"), } +CN_KEYSET_SWITCH_VERSION = (6, 4, 0) + + +def _parse_version(version: str) -> Tuple[int, ...]: + parts = [] + for part in str(version).split("."): + digits = "".join(c for c in part if c.isdigit()) + if not digits: + break + parts.append(int(digits)) + return tuple(parts) + + +def get_keyset(region: str, app_version: Optional[str] = None) -> Tuple[bytes, bytes]: + """Get the APIManager keyset for a region and (optionally) an app version.""" + if region == "cn" and app_version: + parsed = _parse_version(app_version) + if parsed and parsed < CN_KEYSET_SWITCH_VERSION: + return SEKAI_APIMANAGER_KEYSETS["cn-legacy"] + return SEKAI_APIMANAGER_KEYSETS[region] + def encrypt(data: bytes, keyset: Tuple[bytes, bytes]) -> bytes: return encrypt_aes_cbc(data, *keyset, PKCS7_pad)