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)