Merge pull request 'mai2: fix Exp and Chn encryption' (#216) from zaphkito/artemis:develop into develop

Reviewed-on: Hay1tsme/artemis#216
This commit is contained in:
2025-07-13 05:21:19 +00:00
3 changed files with 81 additions and 20 deletions

View File

@ -258,6 +258,31 @@ python dbutils.py upgrade
Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code! Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code!
### Config
Config file is located in `config/mai2.yaml`.
| Option | Info |
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------|
| `crypto` | This option is used to enable the TLS Encryption |
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 (SDEZ) versions and `"{versionID}_int"` for Export (SDGA) versions, and the value
is an array containing `[key, iv, salt]` in order.
Just copy your salt in here, no need to convert anything.
```yaml
crypto:
encrypted_only: False
keys:
23: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"]
"23_int": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"]
"23_chn": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"]
```
## Hatsune Miku Project Diva ## Hatsune Miku Project Diva
### SBZV ### SBZV

View File

@ -46,7 +46,7 @@ allnet:
allow_online_updates: False allow_online_updates: False
update_cfg_folder: "" update_cfg_folder: ""
save_billing: True save_billing: True
allnet_lite_key: [] allnet_lite_keys: []
billing: billing:
standalone: True standalone: True

View File

@ -38,7 +38,7 @@ class Mai2Servlet(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 = Mai2Config() self.game_cfg = Mai2Config()
self.hash_table: Dict[int, Dict[str, str]] = {} self.hash_table: Dict[str, Dict[str, str]] = {}
if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"): if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"):
self.game_cfg.update( self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}")) yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"))
@ -97,16 +97,21 @@ class Mai2Servlet(BaseServlet):
self.logger.initted = True self.logger.initted = True
for version, keys in self.game_cfg.crypto.keys.items(): for version, keys in self.game_cfg.crypto.keys.items():
if version < Mai2Constants.VER_MAIMAI_DX: if int(str(version).split('_')[0]) < Mai2Constants.VER_MAIMAI_DX:
continue continue
if len(keys) < 3: if len(keys) < 3:
continue continue
if isinstance(version, int):
version_idx = version
else:
version_idx = int(version.split("_")[0])
self.hash_table[version] = {} 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("__")
] ]
@ -115,6 +120,21 @@ class Mai2Servlet(BaseServlet):
# remove the first 6 chars and the final 7 chars to get the canonical # remove the first 6 chars and the final 7 chars to get the canonical
# endpoint name. # endpoint name.
method_fixed = inflection.camelize(method)[6:-7] method_fixed = inflection.camelize(method)[6:-7]
# This only applies for maimai DX International and later for some reason.
if (
isinstance(version, str)
and version.endswith("_int")
and version_idx >= Mai2Constants.VER_MAIMAI_DX_UNIVERSE
):
method_fixed += "MaimaiExp"
elif (
isinstance(version, str)
and version.endswith("_chn")
and version_idx >= Mai2Constants.VER_MAIMAI_DX_UNIVERSE # 1.00, 1.11 and 1.20 all use DX, but they add MaimaiChn in 1.20, we set 1.20 to use UNIVERSE code
):
method_fixed += "MaimaiChn"
hash = MD5.new((method_fixed + keys[2]).encode()) hash = MD5.new((method_fixed + keys[2]).encode())
# truncate unused bytes like the game does # truncate unused bytes like the game does
@ -310,7 +330,7 @@ class Mai2Servlet(BaseServlet):
internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES
elif version >= 145 and version <150: # BUDDiES PLUS elif version >= 145 and version <150: # BUDDiES PLUS
internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS
elif version >=150: elif version >= 150: # PRiSM
internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM
elif game_code == "SDGA": # Int elif game_code == "SDGA": # Int
@ -334,47 +354,58 @@ class Mai2Servlet(BaseServlet):
internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES
elif version >= 145 and version <150: # BUDDiES PLUS elif version >= 145 and version <150: # BUDDiES PLUS
internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS
elif version >=150: elif version >= 150: # PRiSM
internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM
elif game_code == "SDGB": # Chn elif game_code == "SDGB": # Chn
if version < 110: # Muji if version < 110: # Muji
internal_ver = Mai2Constants.VER_MAIMAI_DX internal_ver = Mai2Constants.VER_MAIMAI_DX
elif version >= 110 and version < 120: # Muji elif version >= 110 and version < 120: # Muji
internal_ver = Mai2Constants.VER_MAIMAI_DX internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH # still DX, but need Splash to set encryption key
elif version >= 120 and version < 130: # Muji (LMAO) elif version >= 120 and version < 130: # Muji (LMAO)
internal_ver = Mai2Constants.VER_MAIMAI_DX internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE # still DX, but need UNIVERSE to set encryption key
elif version >= 130 and version < 140: # FESTiVAL elif version >= 130 and version < 140: # FESTiVAL
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL
elif version >= 140 and version < 150: # BUDDiES elif version >= 140 and version < 150: # BUDDiES
internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES
elif version >=150: elif version >= 150: # PRiSM
internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM
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 game_code == "SDGA":
crypto_cfg_key = f"{internal_ver}_int"
hash_table_key = f"{internal_ver}_int"
elif game_code == "SDGB":
crypto_cfg_key = f"{internal_ver}_chn"
hash_table_key = f"{internal_ver}_chn"
else:
crypto_cfg_key = internal_ver
hash_table_key = internal_ver
# 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
# dealing with an encrypted request. False positives shouldn't happen # dealing with an encrypted request. False positives shouldn't happen
# as long as requests are suffixed with `Api`. # as long as requests are suffixed with `Api`.
if internal_ver not in self.hash_table: if hash_table_key not in self.hash_table:
self.logger.error( self.logger.error(
"v%s does not support encryption or no keys entered", "v%s does not support encryption or no keys entered",
version, version,
) )
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(
"No hash found for v%s endpoint %s", "No hash found for v%s endpoint %s",
version, endpoint version, 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)
@ -392,7 +423,10 @@ class Mai2Servlet(BaseServlet):
if ( if (
not encrypted not encrypted
and self.game_cfg.crypto.encrypted_only and self.game_cfg.crypto.encrypted_only
and version >= 110 and (
# SDEZ start from 1.10, SDGA and SDGB keep use encryption from 1.00
internal_ver >= Mai2Constants.VER_MAIMAI_DX_PLUS or (game_code == "SDGA" or game_code == "SDGB")
)
): ):
self.logger.error( self.logger.error(
"Unencrypted v%s %s request, but config is set to encrypted only: %r", "Unencrypted v%s %s request, but config is set to encrypted only: %r",
@ -416,9 +450,9 @@ class Mai2Servlet(BaseServlet):
endpoint = ( endpoint = (
endpoint.replace("MaimaiExp", "") endpoint.replace("MaimaiExp", "")
if game_code == Mai2Constants.GAME_CODE_DX_INT if game_code == Mai2Constants.GAME_CODE_DX_INT and version >= 120
else endpoint.replace("MaimaiChn", "") else endpoint.replace("MaimaiChn", "")
if game_code == Mai2Constants.GAME_CODE_DX_CHN if game_code == Mai2Constants.GAME_CODE_DX_CHN and version >= 120
else endpoint else endpoint
) )
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
@ -444,15 +478,17 @@ class Mai2Servlet(BaseServlet):
zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
if not encrypted or version < 110: if not encrypted or (
internal_ver < Mai2Constants.VER_MAIMAI_DX_PLUS and game_code == "SDEZ"
):
return Response(zipped) return Response(zipped)
padded = pad(zipped, 16) padded = pad(zipped, 16)
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]),
) )
return Response(crypt.encrypt(padded)) return Response(crypt.encrypt(padded))