diff --git a/.gitignore b/.gitignore index 31e40848..d69d476a 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,6 @@ src/main/resources/meta/*/*.json test-diff htmlReport docs/logs + +# Sensitive keys +game_encryption.json \ No newline at end of file diff --git a/AquaNet/src/libs/i18n/en_ref.ts b/AquaNet/src/libs/i18n/en_ref.ts index f08fe877..0fb0f272 100644 --- a/AquaNet/src/libs/i18n/en_ref.ts +++ b/AquaNet/src/libs/i18n/en_ref.ts @@ -144,10 +144,9 @@ export const EN_REF_SETUP = { 'setup.welcome': `Welcome! If you have a game set up, please follow the instructions below to set up the connection with AquaDX.`, 'setup.keychip-warning': `Your keychip(s) are linked to your account and should be kept secure.`, 'setup.steps.one': `Pick a method of setting up network communications. Some browsers may not be able to do automatic setup.`, - 'setup.steps.two': `Ensure your game has encryption disabled as AquaDX does not support encryption. This may be via a patch or setting.`, - 'setup.steps.three': `Link your Aime card to your AquaDX account using the Cards page via it's access code or serial number.`, - 'setup.steps.four': `Start the game. Upon reaching the title screen, the network icon in the corner should now show green instead of grey.`, - 'setup.support-info': `If you need assistance, feel free to make an inquiry in a support channel.`, + 'setup.steps.two': `Link your Aime card to your AquaDX account using the Cards page via it's access code or serial number.`, + 'setup.steps.three': `Start the game. Upon reaching the title screen, the network icon in the corner should now show green instead of grey.`, + 'setup.support-info': `If you need additional assistance, feel free to make an inquiry in a support channel.`, 'setup.reveal-keychip': `Reveal keychip`, 'setup.type.automatic': `Automatic Setup`, 'setup.type.manual': `Manual Setup`, @@ -156,6 +155,15 @@ export const EN_REF_SETUP = { 'setup.automatic.success': `Success, data has been updated`, 'setup.automatic.failure': `Failed to update information, please ensure access is not blocked and try again.`, 'setup.automatic.select': `Pick file`, + 'setup.troubleshooting.header': 'Setup Troubleshooting (FAQ)', + 'setup.troubleshooting.items.one': `» It displays DNS BAD on Network Test
+ You likely misconfigured your segatools.ini. Ensure it's pointing to the DNS shown above (under Manual Setup).`, + 'setup.troubleshooting.items.two': `» It displays タイトル サーバー BAD (Title Server BAD) on Network Test
+ You should check your segatools.ini to ensure your keychip is set correctly.
+ Additionally, Card Maker users and players using a game version higher than what's listed may need to disable encryption + (instructions for Card Maker)`, + 'setup.troubleshooting.items.three': `» I can't scan my card
+ The built-in emulated card reader, by default, is configured to use the Enter / Return key to scan in (hold the key).`, 'setup.keychip': 'Keychip Management', 'setup.keychip.warning': 'Keychips can be manually set here for cabinet owners with physical keychips trying to connect to AquaDX. Selecting a keychip will autofill it for the setup below.', 'setup.keychip.add': `Add keychip`, diff --git a/AquaNet/src/libs/i18n/zh.ts b/AquaNet/src/libs/i18n/zh.ts index e5ee5412..b997bf7a 100644 --- a/AquaNet/src/libs/i18n/zh.ts +++ b/AquaNet/src/libs/i18n/zh.ts @@ -5,6 +5,7 @@ import { EN_REF_LEADERBOARD, EN_REF_MAI_PHOTO, EN_REF_SETTINGS, + EN_REF_SETUP, EN_REF_USER, EN_REF_USERBOX, type EN_REF_Welcome @@ -59,15 +60,15 @@ const zhWelcome: typeof EN_REF_Welcome = { 'welcome.username-missing': '用户名/邮箱必须填哦', 'welcome.email-password-missing': '邮箱和密码必须填哦', 'welcome.waiting-turnstile': '正在验证网络环境…', - 'welcome.turnstile-error': '验证网络环境出错了,请关闭 VPN 后重试', - 'welcome.turnstile-timeout': '验证网络环境超时了,请重试', - 'welcome.verification-sent': '验证邮件已发送至 ${email},请翻翻收件箱', - 'welcome.reset-password-sent': '重置邮件已发送至 ${email},请翻翻收件箱', - 'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱,请翻翻收件箱', - 'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的 24 小时内已经发送了 3 封验证邮件,所以我们不会再发送了,请翻翻收件箱', - 'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件,请翻翻收件箱', - 'welcome.reset-state-0': '重置邮件刚刚发送到你的邮箱啦,请翻翻收件箱!', - 'welcome.reset-state-1': '邮件发送次数过多,暂时不会再发送新的重置邮件了', + 'welcome.turnstile-error': '验证网络环境出错了,请关闭 VPN 后重试', + 'welcome.turnstile-timeout': '验证网络环境超时了,请重试', + 'welcome.verification-sent': '验证邮件已发送至 ${email},请翻翻收件箱', + 'welcome.reset-password-sent': '重置邮件已发送至 ${email},请翻翻收件箱', + 'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱,请翻翻收件箱', + 'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的 24 小时内已经发送了 3 封验证邮件,所以我们不会再发送了,请翻翻收件箱', + 'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件,请翻翻收件箱', + 'welcome.reset-state-0': '重置邮件刚刚发送到你的邮箱啦,请翻翻收件箱!', + 'welcome.reset-state-1': '邮件发送次数过多,暂时不会再发送新的重置邮件了', 'welcome.verifying': '正在验证邮箱…请稍等', 'welcome.verified': '您的邮箱已经验证成功!您现在可以登录了', 'welcome.verification-failed': '验证失败:${message}。请重试', @@ -90,7 +91,7 @@ const zhGeneral: typeof EN_REF_GENERAL = { 'game.ongeki': "音击", 'game.wacca': "华卡", "status.error": "发生错误", - "status.error.hint": "出了一些问题,请稍后刷新重试或者", + "status.error.hint": "出了一些问题,请稍后刷新重试或者", "status.error.hint.link": "加我们的 Discord 群问一问", "status.detail": "详细信息:${detail}", "action.refresh": "刷新", @@ -112,21 +113,21 @@ const zhHome: typeof EN_REF_HOME = { 'home.link-card': '绑定游戏卡', 'home.link-cards-description':'绑定游戏数据卡(Amusement IC 或 Aime 卡)后才可以访问游戏存档哦', 'home.join-community': '加入群组', - 'home.join-community-description': '加入我们的聊天群组,与其他玩家聊天、获取帮助', + 'home.join-community-description': '加入我们的聊天群组,与其他玩家聊天、获取帮助', 'home.setup': '连接到 AquaDX', - 'home.setup-description': '如果您有街机框体或者手台,点击这里设置服务器的连接', + 'home.setup-description': '如果您有街机框体或者手台,点击这里设置服务器的连接', 'home.import': '导入玩家数据', - 'home.import-description': '如果你来自其他在线服,可以点击这里导入从其他服务器导出的数据', + 'home.import-description': '如果你来自其他在线服,可以点击这里导入从其他服务器导出的数据', 'home.linkcard.cards': "已绑卡片", 'home.linkcard.description': "这些是您绑定到帐户的卡", 'home.linkcard.account-card': "账户卡", 'home.linkcard.registered': "注册于", 'home.linkcard.lastused': "上次使用", - 'home.linkcard.enter-info': "请输入以下信息,或将 aime.txt / felica.txt 文件拖放到此区域", - 'home.linkcard.access-code': "卡背面的 20 位卡号(如果提示找不到卡,请尝试使用游戏内置的显示卡号功能,输入游戏读取到的卡号)", + 'home.linkcard.enter-info': "请输入以下信息,或将 aime.txt / felica.txt 文件拖放到此区域", + 'home.linkcard.access-code': "卡背面的 20 位卡号(如果提示找不到卡,请尝试使用游戏内置的显示卡号功能,输入游戏读取到的卡号)", 'home.linkcard.enter-sn1': "在您的手机", 'home.linkcard.enter-sn2': "上下载 NFC Tools 并扫描您的卡。然后输入显示的 SN 号。", - 'home.linkcard.kdx-notice': "如果你在玩 KanadeDX,请在这里输入虚拟卡号(可以在设置 > 卡号中找到卡号)", + 'home.linkcard.kdx-notice': "如果你在玩 KanadeDX,请在这里输入虚拟卡号(可以在设置 > 卡号中找到卡号)", 'home.linkcard.link': "绑定", 'home.linkcard.data-conflict': "卡号冲突", 'home.linkcard.name': "名称", @@ -137,15 +138,15 @@ const zhHome: typeof EN_REF_HOME = { 'home.linkcard.notfound': "找不到卡", 'home.linkcard.unlink': "取消链接", 'home.linkcard.unlink-notice': "你确定要取消此卡的链接吗?", - 'home.linkcard.felica-ac-warning': "该 Access Code 是一张 FeliCa AIC 卡。\n如果你使用实体卡(而非 aime.txt 模拟)刷卡登录游戏,与官方服务器不同,你需要绑定该卡的 FeliCa SN(或与之对应的,游戏界面中查看得到的 00 开头的卡号)而非此号码。\n如果你使用 aime.txt 模拟登录,请忽略本警告继续绑定。", - 'home.setup.welcome': "欢迎! 如果您有街机框体或者手台,请按照以下说明设置以连接到 AquaDX。", - 'home.setup.blockquote': "我们假设您已经拥有所需的文件,并且可以启动机台或手台附带的游戏(例如 ROM 和 segatools)。如果没有,请联系您设备的卖家以获取所需的文件,因为出于版权原因,我们不会提供这些文件。", + 'home.linkcard.felica-ac-warning': "该 Access Code 是一张 FeliCa AIC 卡。\n如果你使用实体卡(而非 aime.txt 模拟)刷卡登录游戏,与官方服务器不同,你需要绑定该卡的 FeliCa SN(或与之对应的,游戏界面中查看得到的 00 开头的卡号)而非此号码。\n如果你使用 aime.txt 模拟登录,请忽略本警告继续绑定。", + 'home.setup.welcome': "欢迎! 如果您有街机框体或者手台,请按照以下说明设置以连接到 AquaDX。", + 'home.setup.blockquote': "我们假设您已经拥有所需的文件,并且可以启动机台或手台附带的游戏(例如 ROM 和 segatools)。如果没有,请联系您设备的卖家以获取所需的文件,因为出于版权原因,我们不会提供这些文件。", 'home.setup.get': "开始", 'home.setup.edit': "请打开您的 segatools.ini 文件并修改以下行", - 'home.setup.test': "在您重新启动游戏后,应该能够连接到 AquaDX。可以验证测试菜单中的网络测试测试连接是否全部良好。", - 'home.setup.ask': "如果您有任何问题,请加入我们的", + 'home.setup.test': "在您重新启动游戏后,应该能够连接到 AquaDX。可以验证测试菜单中的网络测试测试连接是否全部良好。", + 'home.setup.ask': "如果您有任何问题,请加入我们的", 'home.setup.support': "以获取支持", - 'home.setup.keychip-tips': "这是你的狗号,不要与任何人分享", + 'home.setup.keychip-tips': "这是你的狗号,不要与任何人分享", 'home.community.discord': 'Discord', 'home.community.telegram': 'Telegram (中文)', 'home.community.qq': 'QQ (中文)', @@ -154,6 +155,37 @@ const zhHome: typeof EN_REF_HOME = { 'home.import.data-conflict': '继续导入将覆盖现有数据', } +const zhSetup: typeof EN_REF_SETUP = { + 'setup.welcome': `欢迎!如果您已准备好游戏,请按照以下说明设置与 AquaDX 的连接。`, + 'setup.keychip-warning': `您的密钥芯片已与您的账户绑定,请妥善保管。`, + 'setup.steps.one': `选择一种网络通信设置方法。某些浏览器可能无法进行自动设置。`, + 'setup.steps.two': `请通过 卡片 页面,使用访问码或序列号将您的 Aime 卡与 AquaDX 账户绑定。`, + 'setup.steps.three': `启动游戏。进入标题画面后,角落处的网络图标应变为绿色,而非灰色。`, + 'setup.support-info': `如需进一步协助,欢迎在 支持频道 中咨询。`, + 'setup.reveal-keychip': `显示密钥芯片`, + 'setup.type.automatic': `自动设置`, + 'setup.type.manual': `手动设置`, + 'setup.manual': `请使用以下信息修改您的 segatools.ini 文件。`, + 'setup.automatic': `请在下方的 segatools.ini 文件中选择,以自动填写网络信息。`, + 'setup.automatic.select': `选择文件`, + 'setup.automatic.success': `成功,数据已更新`, + 'setup.automatic.failure': `更新信息失败,请确保访问未被阻止,然后重试。`, + 'setup.troubleshooting.header': '设置故障排除(常见问题解答)', + 'setup.troubleshooting.items.one': `» 网络测试显示 DNS BAD
+ 您可能配置了错误的 segatools.ini。请确保其指向上述(手动设置部分)显示的 DNS。`, + 'setup.troubleshooting.items.two': `» 网络测试显示 タイトル サーバー BAD (Title Server BAD)
+ 请检查您的 segatools.ini 文件,确保密钥芯片设置正确。
+ 此外, Card Maker 用户以及使用高于列表中版本的游戏版本的玩家,可能需要禁用加密 + (Card Maker 操作指南)`, + 'setup.troubleshooting.items.three': `» 无法扫描卡片
+ 内置的模拟读卡器默认配置为使用回车键进行扫描(长按该键)。`, + 'setup.keychip': '钥匙芯片管理', + 'setup.keychip.warning': '对于拥有实体钥匙芯片并试图连接到 AquaDX 的机柜所有者,可在此手动设置钥匙芯片。选择钥匙芯片后,下方设置项将自动填充。', + 'setup.keychip.add': `添加钥匙芯片`, + 'setup.keychip.delete': `删除`, + 'setup.keychip.placeholder': `新钥匙芯片 ID` +} + const zhSettings: typeof EN_REF_SETTINGS = { 'settings.title': '用户设置', 'settings.tabs.profile': '个人资料', @@ -163,7 +195,7 @@ const zhSettings: typeof EN_REF_SETTINGS = { 'settings.tabs.ongeki': '音击', 'settings.tabs.wacca': '华卡', - "settings.options.all.preset-warning": "您正在使用预设选项配置,部分选项可能显示不正确。", + "settings.options.all.preset-warning": "您正在使用预设选项配置,部分选项可能显示不正确。", "settings.options.all.personal-best": "记录", "settings.options.all.rival-score": "对手", "settings.options.all.life": "生命值", @@ -220,11 +252,11 @@ const zhSettings: typeof EN_REF_SETTINGS = { 'settings.fields.optOutOfLeaderboard.name': '不参与排行榜', 'settings.fields.optOutOfLeaderboard.desc': '登录之后还是可以在排行榜上看到自己', 'settings.fields.enableMusicRank.name': '在你的机台上启用「推荐乐曲排行榜」', - 'settings.fields.enableMusicRank.desc': '如果你自己设计了排行榜的话,可以关闭这个(会影响你自己的机器)。', + 'settings.fields.enableMusicRank.desc': '如果你自己设计了排行榜的话,可以关闭这个(会影响你自己的机器)。', 'settings.mai2.name': '玩家名字', 'settings.profile.picture': '头像', 'settings.profile.upload-new': '上传', - 'settings.profile.bad-format': '无效的图片格式,支持的格式有 PNG、JPG、JPEG、WEBP 和 GIF。', + 'settings.profile.bad-format': '无效的图片格式,支持的格式有 PNG、JPG、JPEG、WEBP 和 GIF。', 'settings.profile.save': '保存', 'settings.profile.name': '昵称', 'settings.profile.username': '用户名', @@ -237,7 +269,7 @@ const zhSettings: typeof EN_REF_SETTINGS = { 'settings.profile.unchanged': '未更改', 'settings.export': '导出玩家数据', 'settings.batchManualExport': "导出 Batch Manual 格式(用于 Tachi)", - 'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置', + 'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置', // AI 'settings.siteNotice': "这些设置仅适用于网站。", // AI @@ -284,7 +316,7 @@ export const zhUserbox: typeof EN_REF_USERBOX = { 'userbox.matching.symbolChat.default': '默认', 'userbox.new.name': 'AquaBox', - 'userbox.new.setup': '将中二(Lumi 或更高版本)的游戏文件夹拖放到下方区域,以显示带有名牌和头像的 UserBox。所有文件都在浏览器中处理。', + 'userbox.new.setup': '将中二(Lumi 或更高版本)的游戏文件夹拖放到下方区域,以显示带有名牌和头像的 UserBox。所有文件都在浏览器中处理。', 'userbox.new.setup.notice': '选择包含游戏数据的最外层文件夹。', 'userbox.new.setup.processing_file': '正在处理文件', 'userbox.new.setup.finalizing': '正在保存到内部存储', @@ -298,20 +330,20 @@ export const zhUserbox: typeof EN_REF_USERBOX = { 'userbox.new.activate_desc': '启用后可显示带有名牌和头像的 UserBox', 'userbox.new.activate_profile': '在用户页面启用 AquaBox', 'userbox.new.activate_profile_desc': '启用后可在个人页面显示带有名牌和头像的 UserBox', - 'userbox.new.error.invalidFolder': '所选文件夹无效。请确认游戏版本为 Lumi 或更新,并且包含 “A000” 选项包。', + 'userbox.new.error.invalidFolder': '所选文件夹无效。请确认游戏版本为 Lumi 或更新,并且包含 “A000” 选项包。', 'userbox.new.error.invalidUrl': '输入的 URL 无效。' }; export const zhMaiPhoto: typeof EN_REF_MAI_PHOTO = { 'maiphoto.title': 'Mai 纪念照片库', - 'maiphoto.url_warning': '注意:如果想与朋友分享图片的话,请先保存照片再发出去。不要复制图片 URL,因为 URL 中包含 AquaDX 账号信息。', + 'maiphoto.url_warning': '注意:如果想与朋友分享图片的话,请先保存照片再发出去。不要复制图片 URL,因为 URL 中包含 AquaDX 账号信息。', 'maiphoto.none': '还没有图片哦~ 可以在每次游戏结束的时候点击上传来上传照片。', } export const zhAquaTrans: typeof EN_REF_AQUATRANS = { 'trans.title': '🏳️‍⚧️ AquaTrans™ 数据迁移工具', 'trans.confirm.unbackuped.title': '确认迁移', - 'trans.confirm.unbackuped.msg': '似乎还没有备份目标服务器的数据,真的要继续吗?(推荐先备份一下,因为迁移的时候会覆盖数据)', + 'trans.confirm.unbackuped.msg': '似乎还没有备份目标服务器的数据,真的要继续吗?(推荐先备份一下,因为迁移的时候会覆盖数据)', 'trans.confirm.untested.title': '不太聪明喵', 'trans.confirm.untested.msg': '在两个服务器上都测试完连接之后才能进行数据迁移哦!', 'trans.confirm.done.title': '完成!', @@ -319,8 +351,8 @@ export const zhAquaTrans: typeof EN_REF_AQUATRANS = { 'trans.alert.in-progress': '在迁移了在迁移了', 'trans.prompt-html': `

👋 欢迎使用 AquaTrans™ 服务器游玩数据迁移工具!

-

这个工具可以导出任意服务器的数据,并使用连接凭证(卡号、服务器地址和 Keychip ID)将数据导入任何其他服务器。

-

我将模拟游戏客户端,从源服务器拉取游戏数据并推送到目标服务器。

+

这个工具可以导出任意服务器的数据,并使用连接凭证(卡号、服务器地址和 Keychip ID)将数据导入任何其他服务器。

+

我将模拟游戏客户端,从源服务器拉取游戏数据并推送到目标服务器。

填写下面的表格开始迁移吧!

`, 'trans.error.empty': '请填写所有字段。', @@ -336,11 +368,11 @@ export const zhAquaTrans: typeof EN_REF_AQUATRANS = { 'trans.btn.test': '测试连接', 'trans.btn.export': '导出数据', 'trans.btn.import': '导入数据', - 'trans.blacklist': "这个服务器的服主把这个导出工具 ban 了,所以不能从这里导出", + 'trans.blacklist': "这个服务器的服主把这个导出工具 ban 了,所以不能从这里导出", } export const ZH = { ...zhUser, ...zhWelcome, ...zhGeneral, ...zhLeaderboard, ...zhHome, ...zhSettings, ...zhUserbox, ...zhMaiPhoto, - ...zhAquaTrans + ...zhAquaTrans, ...zhSetup } diff --git a/AquaNet/src/pages/Home/SetupInstructions.svelte b/AquaNet/src/pages/Home/SetupInstructions.svelte index 11141c9a..6f5ba7f0 100644 --- a/AquaNet/src/pages/Home/SetupInstructions.svelte +++ b/AquaNet/src/pages/Home/SetupInstructions.svelte @@ -201,10 +201,18 @@ ${keychipLines}`.trim(), {
3.
{@html t('setup.steps.three')}
-
- 4.
{@html t('setup.steps.four')}
-
- +

{t('setup.troubleshooting.header')}

+

{@html t('setup.support-info')}

@@ -217,6 +225,9 @@ ${keychipLines}`.trim(), { .code overflow-x: auto + ul + li + margin: 0.75em 0 .divider width: 90% height: 1px diff --git a/README.md b/README.md index a099a9ee..0f655f45 100644 --- a/README.md +++ b/README.md @@ -32,21 +32,22 @@ Multipurpose game server for ALL.Net games. > [!WARNING] > CHUNITHM pre-NEW!! and maimai pre-DX are no longer supported after February 6th, 2026 and all associated data will be removed. -| Game | Latest Ver. | Initial Ver. | Notes | Web UI | Data Import | -|------------------------|---------------------|---------------------|-------------------------------------------------------------|--------|-------------| -| SDHD: CHUNITHM | 2.45 (X-VERSE-X) | 2.00 (NEW) | | ✅ | ❗ | -| SDEZ: maimai DX | 1.65 (CiRCLE PLUS) | 1.00 (DX) | Missing circle (teams) support | ✅ | ✅ | -| SDGA: maimai DX (Intl) | 1.60 (CiRCLE) | 1.00 (DX) | Thanks [@Clansty](https://github.com/clansty) | ✅ | ✅ | -| SDED: Card Maker | 1.39 | N/A | Thanks [@Becods](https://github.com/Becods) | ❌ | ❌ | -| SDDT: O.N.G.E.K.I. | 1.50 (Re:Fresh) | N/A | Thanks [@PenguinCaptain](https://github.com/PenguinCaptain) | ✅ | ❌ | -| SBZV: Project DIVA | 7.10 | N/A | Minimal support | ❌ | ❌ | -| SDFE: Wacca | 3.07 (Reverse) | N/A | Later versions are EOS patches, network will not work | ✅ | ❌ | +| Game | Latest Ver. | Initial Ver. | Notes | Web UI | Import | Encrypt | +|------------------------|---------------------|---------------------|-------------------------------------------------------------|--------|-------------|---------| +| SDHD: CHUNITHM | 2.45 (X-VERSE-X) | 2.00 (NEW) | | ✅ | ❗ | ✅ | +| SDEZ: maimai DX | 1.65 (CiRCLE PLUS) | 1.00 (DX) | Missing circle (teams) support | ✅ | ✅ | ✅ | +| SDGA: maimai DX (Intl) | 1.60 (CiRCLE) | 1.00 (DX) | Thanks [@Clansty](https://github.com/clansty) | ✅ | ✅ | ✅ | +| SDED: Card Maker | 1.39 | N/A | Thanks [@Becods](https://github.com/Becods) | ❌ | ❌ | ❌ | +| SDDT: O.N.G.E.K.I. | 1.50 (Re:Fresh) | N/A | Thanks [@PenguinCaptain](https://github.com/PenguinCaptain) | ✅ | ❌ | ✅ | +| SBZV: Project DIVA | 7.10 | N/A | Minimal support | ❌ | ❌ | 🔓 | +| SDFE: Wacca | 3.07 (REVERSE) | N/A | Later versions are EOS patches | ✅ | ❌ | 🔓 | Check out these docs for more information. * [Game specific notes](docs/game_specific_notes.md) * [Frequently asked questions](docs/frequently_asked_questions.md) +* [Encryption notes](docs/encryption.md) ## Usage diff --git a/config/application.properties b/config/application.properties index 2a55c13a..7b63286f 100644 --- a/config/application.properties +++ b/config/application.properties @@ -142,4 +142,4 @@ aqua-net.bot.enabled=true aqua-net.bot.secret=hunter2 ## OpenAI Settings (For content moderation) -aqua-net.openai.api-key=sk-1234567890abcdef1234567890abcdef +aqua-net.openai.api-key=sk-1234567890abcdef1234567890abcdef \ No newline at end of file diff --git a/docs/encryption.md b/docs/encryption.md new file mode 100644 index 00000000..50437860 --- /dev/null +++ b/docs/encryption.md @@ -0,0 +1,35 @@ +# Encryption Support + +AquaDX supports encryption for Chunithm, Maimai and Ongeki.
+It is not required to connect and keys are NOT provided by default. + +Card Maker has encryption features in the client, but they are not supported due to it's complexity. + +## Adding new keys to AquaDX + +> [!IMPORTANT] +> Ensure you're using AquaDX from source. Docker will work, as long as you use `docker-compose up --build` + +1. Create a new file at `src/main/resources/data/game//game_encryption.json` +2. Fill it out using the following schema: +```json +{ + "code": "SDHD", + "versions": [215, 216], + "key": "DECAFBADDECAFBADDECAFBADDECAFBADDECAFBADDECAFBADDECAFBADDECAFBAD", + "iv": "DECAFBADDECAFBADDECAFBADDECAFBAD", + "salt": "DECAFBADDECAFBAD", + "iterations": 36 +}, +``` + +**`key`, `salt`, and `iv` MUST be hex strings. Do NOT insert raw ASCII strings.**
+ +Replace `code` with the correct code for your game (`SDHD` for Chunithm JP, `SDEZ` for Maimai JP, etc.).
+Set `versions` to be an array of game versions without the decimal (215 for 2.15, 150 for 1.50, etc.).
+You MUST include rom patch versions in this array or it will not encrypt correctly. + +- Maimai DX does NOT use iterations, you do not need to include it +- Ongeki always uses 64 iterations (you must include it) +- If using a specific Python script to rip Chunithm keys, focus on the `=>` entries. +- `key` is 32 bytes long. `iv` is 16 bytes long. `salt` is 8 bytes long. (hex strings should be twice as long as they are in bytes) \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt b/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt index ef977362..bc89868f 100644 --- a/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt +++ b/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt @@ -185,20 +185,7 @@ class AllNet( // If keychip authentication is enabled, the game URLs will be set to /gs/{token}/{game}/... val base = if (session != null) "gs/$session" else "g" - return "http://$addr/$base/" + when (gameId) { - "SDBT" -> "chu2/$ver/$session/" - "SDHD" -> "chu3/$ver/" - "SDGS" -> "chu3/$ver/" // International (c3exp) - "SBZV" -> "diva/" - "SDDT" -> "ongeki/$ver/" - "SDEY" -> "mai/" - "SDGA" -> "mai2/" // International (Exp) - "SDGB" -> "mai2/" // International (China) - TODO: Test it - "SDEZ" -> "mai2/" - "SDFE" -> "wacca" // Note: Wacca must not end with a trailing slash - "SDED" -> "card/" - else -> "" - } + return "http://$addr/$base/$gameId/$ver/" } companion object { diff --git a/src/main/java/icu/samnyan/aqua/sega/cardmaker/CardMakerController.kt b/src/main/java/icu/samnyan/aqua/sega/cardmaker/CardMakerController.kt index 489b07cb..6f06f38d 100644 --- a/src/main/java/icu/samnyan/aqua/sega/cardmaker/CardMakerController.kt +++ b/src/main/java/icu/samnyan/aqua/sega/cardmaker/CardMakerController.kt @@ -19,7 +19,7 @@ import java.time.format.DateTimeFormatter * @author samnyan (privateamusement@protonmail.com) */ @RestController -@RequestMapping("/g/card") +@RequestMapping("/g/SDED/{version}") class CardMakerController( val mapper: BasicMapper, @param:Value("\${allnet.server.host:}") val ALLNET_HOST: String, diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/ChusanController.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/ChusanController.kt index 714580eb..4bbaa203 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/ChusanController.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/ChusanController.kt @@ -17,13 +17,18 @@ import icu.samnyan.aqua.spring.Metrics import jakarta.servlet.http.HttpServletRequest import org.slf4j.LoggerFactory import org.springframework.web.bind.annotation.RestController +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.PBEKeySpec /** * @author samnyan (privateamusement@protonmail.com) */ @Suppress("unused") @RestController -@API(value = ["/g/chu3/{version}/ChuniServlet", "/g/chu3/{version}"]) +@API(value = [ + "/g/SDHD/{version}/ChuniServlet", "/g/SDHD/{version}", + "/g/SDGS/{version}/ChuniServlet", "/g/SDGS/{version}", +]) class ChusanController( val mapper: StringMapper, val cmMapper: BasicMapper, @@ -37,14 +42,19 @@ class ChusanController( ): MeowApi({ api, resp -> if (resp is String) resp else (if ("CM" in api || api == "GetUserItemApi" ) cmMapper else mapper).write(resp) +}, +@kotlin.ExperimentalStdlibApi +{ endpoint: String -> + db.gameData.chu3GameEncryption.map{ + val suffix = when (it.code) { "SDGS" -> "C3Exp" else -> "" } + SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") + .generateSecret( + PBEKeySpec((endpoint + suffix).toCharArray(), it.salt!!.hexToByteArray(), it.iterations, 128) + ).encoded.toHexString().substring(0, 32) + } }) { val log = LoggerFactory.getLogger(ChusanController::class.java) - val noopEndpoint = setOf("UpsertClientBookkeepingApi", "UpsertClientDevelopApi", "UpsertClientErrorApi", - "UpsertClientSettingApi", "UpsertClientTestmodeApi", "CreateTokenApi", "RemoveTokenApi", "UpsertClientUploadApi", - "PrinterLoginApi", "PrinterLogoutApi", "Ping", "GameLogoutApi", "RemoveMatchingMemberApi", - "UpsertClientPlayTimeApi", "UpsertClientGameStartApi", "UpsertClientGameEndApi") - init { chusanInit() } val handlers = initH @@ -60,28 +70,27 @@ class ChusanController( data["c3exp"] = true } + val apiFriendlyName = if (endpoint.lowercase() == endpoint && endpoint.length == 32) + hashes.filter { it.value.contains(api) }.keys.first().str + " (encrypt)" else api + if (api.startsWith("CM") && api !in handlers) api = api.removePrefix("CM") val token = TokenChecker.tokenShort() - log.info("$token : $api < ${data.toJson()}") + log.info("$token : $apiFriendlyName < ${data.toJson()}") val noop = """{"returnCode":"1","apiName":"$api"}""" - if (api !in noopEndpoint && !handlers.containsKey(api)) { - log.warn("$token : $api > not found") + if (!handlers.containsKey(api)) { + log.warn("$token : $apiFriendlyName > not found") return noop } // Only record the counter metrics if the API is known. Metrics.counter("aquadx_chusan_api_call", "api" to api).increment() - if (api in noopEndpoint) { - log.info("$token : $api > no-op") - return noop - } return try { Metrics.timer("aquadx_chusan_api_latency", "api" to api).recordCallable { serialize(api, handlers[api]!!(ctx) ?: noop).also { if (api !in setOf("GetUserItemApi", "GetGameEventApi")) - log.info("$token : $api > ${it.truncate(500)}") + log.info("$token : $apiFriendlyName > ${it.truncate(500)}") } } } catch (e: Exception) { diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt index 07371a45..45485d6a 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt @@ -431,5 +431,23 @@ fun ChusanController.chusanInit() { // process() """{"returnCode":"1"}""" + + // NOTE: no-op APIs moved to respect encryption + "UpsertClientBookkeeping" static { """{returnCode":1, "apiName":"UpsertClientBookkeepingApi"}""" } + "UpsertClientDevelop" static { """{returnCode":1, "apiName":"UpsertClientDevelopApi"}""" } + "UpsertClientError" static { """{returnCode":1, "apiName":"UpsertClientErrorApi"}""" } + "UpsertClientSetting" static { """{returnCode":1, "apiName":"UpsertClientSettingApi"}""" } + "UpsertClientTestmode" static { """{returnCode":1, "apiName":"UpsertClientTestmodeApi"}""" } + "CreateToken" static { """{returnCode":1, "apiName":"CreateTokenApi"}""" } + "RemoveToken" static { """{returnCode":1, "apiName":"RemoveTokenApi"}""" } + "UpsertClientUpload" static { """{returnCode":1, "apiName":"UpsertClientUploadApi"}""" } + "PrinterLogin" static { """{returnCode":1, "apiName":"PrinterLoginApi"}""" } + "PrinterLogout" static { """{returnCode":1, "apiName":"PrinterLogoutApi"}""" } + "Ping" static { """{returnCode":1, "apiName":"Ping"}""" } + "GameLogout" static { """{returnCode":1, "apiName":"GameLogoutApi"}""" } + "RemoveMatchingMember" static { """{returnCode":1, "apiName":"RemoveMatchingMemberApi"}""" } + "UpsertClientPlayTime" static { """{returnCode":1, "apiName":"UpsertClientPlayTimeApi"}""" } + "UpsertClientGameStart" static { """{returnCode":1, "apiName":"UpsertClientGameStartApi"}""" } + "UpsertClientGameEnd" static { """{returnCode":1, "apiName":"UpsertClientGameEndApi"}""" } } } \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/DivaController.kt b/src/main/java/icu/samnyan/aqua/sega/diva/DivaController.kt index 7e349215..1cab32d8 100644 --- a/src/main/java/icu/samnyan/aqua/sega/diva/DivaController.kt +++ b/src/main/java/icu/samnyan/aqua/sega/diva/DivaController.kt @@ -35,7 +35,7 @@ val DIVA_INIT = mapOf("db_close" to "0,0", "retry_time" to "FFFF") * @author samnyan (privateamusement@protonmail.com) */ @RestController -@RequestMapping("/g/diva") +@RequestMapping("/g/SBZV/{version}") class DivaController( val attendHandler: AttendHandler, val cardProcedureHandler: CardProcedureHandler, diff --git a/src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt b/src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt index 6bda26e1..34bd81f0 100644 --- a/src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt +++ b/src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt @@ -31,11 +31,23 @@ typealias PagePost = (MutJDict) -> Unit data class PagedProcessor(val add: JDict?, val fn: PagedHandler, var post: PagePost? = null) // A very :3 way of declaring APIs -abstract class MeowApi(val serialize: (String, Any) -> String) { +abstract class MeowApi( + val serialize: (String, Any) -> String, + val endpointHash: ((String) -> List)? = null +) { val initH = mutableMapOf() + val hashes = mutableMapOf>() infix operator fun String.invoke(fn: SpecialHandler) { if (initH.containsKey("${this}Api")) error("Duplicate API $this found! Someone is not smart 👀") initH["${this}Api"] = fn + if (endpointHash != null) { + // Keep track of hashes for logging; they're not necessary for anything else + hashes["${this}Api"] = mutableListOf() + for (hashedEndpoint in endpointHash("${this}Api")) { + initH[hashedEndpoint] = fn + hashes["${this}Api"]?.add(hashedEndpoint) + } + } } infix fun String.static(fn: () -> Any) = serialize(this, fn()).let { resp -> this { resp } } diff --git a/src/main/java/icu/samnyan/aqua/sega/general/filter/CompressionFilter.kt b/src/main/java/icu/samnyan/aqua/sega/general/filter/CompressionFilter.kt index a4242e2c..2e8594df 100644 --- a/src/main/java/icu/samnyan/aqua/sega/general/filter/CompressionFilter.kt +++ b/src/main/java/icu/samnyan/aqua/sega/general/filter/CompressionFilter.kt @@ -5,6 +5,8 @@ import ext.logger import ext.toJson import icu.samnyan.aqua.net.components.GeoIP import icu.samnyan.aqua.sega.allnet.TokenChecker +import icu.samnyan.aqua.sega.general.model.GameEncryptionKey +import icu.samnyan.aqua.sega.util.GameDataService import icu.samnyan.aqua.sega.util.ZLib import jakarta.servlet.FilterChain import jakarta.servlet.http.HttpServletRequest @@ -14,6 +16,9 @@ import org.springframework.stereotype.Component import org.springframework.web.filter.OncePerRequestFilter import org.springframework.web.util.ContentCachingResponseWrapper import java.util.* +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec /** @@ -21,21 +26,46 @@ import java.util.* */ @Component class CompressionFilter( - val geoip: GeoIP + val geoip: GeoIP, + val gameData: GameDataService ) : OncePerRequestFilter() { companion object { val log = logger() val b64d = Base64.getMimeDecoder() val b64e = Base64.getMimeEncoder() + } + var keys = gameData.chu3GameEncryption + gameData.mai2GameEncryption + gameData.ogkGameEncryption + fun getEncryptionKeys(path: String): GameEncryptionKey? { + val endpoint = path.split("/").last() + if (endpoint.lowercase() != endpoint || endpoint.length != 32) return null + + val game = path.split("/")[2] + val version = path.split("/")[3].filterNot { it == '.' }.toInt() + return keys.find { it.versions.contains(version) && it.code == game } + } + + @OptIn(ExperimentalStdlibApi::class) override fun doFilterInternal(req: HttpServletRequest, resp: HttpServletResponse, chain: FilterChain) { val isDeflate = req.getHeader("content-encoding") == "deflate" val isDfi = req.getHeader("pragma") == "DFI" + val keys = getEncryptionKeys(req.servletPath) + // Decode input val reqSrc = try { req.inputStream.readAllBytes().let { + if (keys != null) { + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init( + Cipher.DECRYPT_MODE, + SecretKeySpec(keys.key!!.hexToByteArray(), "AES"), + IvParameterSpec(keys.iv!!.hexToByteArray()) + ) + cipher.doFinal(it) + } else it + }.let { if (isDeflate) ZLib.decompress(it) else if (isDfi) ZLib.decompress(b64d.decode(it)) else it @@ -50,7 +80,19 @@ class CompressionFilter( val respW = ContentCachingResponseWrapper(resp) val result = try { chain.doFilter(CompressRequestWrapper(req, reqSrc), respW) - ZLib.compress(respW.contentAsByteArray).let { if (isDfi) b64e.encode(it) else it } + ZLib.compress(respW.contentAsByteArray) + .let { if (isDfi) b64e.encode(it) else it } + .let { + if (keys != null) { + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init( + Cipher.ENCRYPT_MODE, + SecretKeySpec(keys.key!!.hexToByteArray(), "AES"), + IvParameterSpec(keys.iv!!.hexToByteArray()) + ) + cipher.doFinal(it) + } else it + } } finally { if (respW.status != 200) { val details = mapOf( diff --git a/src/main/java/icu/samnyan/aqua/sega/general/model/EncryptionKey.kt b/src/main/java/icu/samnyan/aqua/sega/general/model/EncryptionKey.kt new file mode 100644 index 00000000..78123f70 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/general/model/EncryptionKey.kt @@ -0,0 +1,14 @@ +package icu.samnyan.aqua.sega.general.model + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty +import java.time.LocalDateTime + +class GameEncryptionKey { + var code: String? = null + var versions: List = listOf() + var key: String? = null + var salt: String? = null + var iv: String? = null + var iterations = 0 +} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt index a8406721..e547ba4e 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt @@ -424,8 +424,20 @@ fun Maimai2ServletController.initApis() { ) ) } - "UpsertUserPlaceCircleRegist" static { mapOf( - "returnCode" to 0, - "apiName" to "UpsertUserPlaceCircleRegistApi" - ) } + // NOTE: no-op APIs moved to respect encryption + "UpsertUserPlaceCircleRegist" static { mapOf( "returnCode" to 0, "apiName" to "com.sega.maimai2servlet.api.UpsertUserPlaceCircleRegistApi") } + "GetUserScoreRanking" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.GetUserScoreRankingApi"}""" } + "UpsertClientBookkeeping" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientBookkeepingApi"}""" } + "UpsertClientSetting" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientSettingApi"}""" } + "UpsertClientTestmode" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientTestmodeApi"}""" } + "UpsertClientUpload" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientUploadApi"}""" } + "Ping" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.Ping"}""" } + "RemoveToken" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.RemoveTokenApi"}""" } + "CMLogin" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.CMLoginApi"}""" } + "CMLogout" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.CMLogoutApi"}""" } + "CMUpsertBuyCard" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.CMUpsertBuyCardApi"}""" } + "UserLogout" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UserLogoutApi"}""" } + "GetGameMapAreaCondition" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.GetGameMapAreaConditionApi"}""" } + "UpsertUserChargelog" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertUserChargelogApi"}""" } + "UpsertClientPlayTime" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientPlayTimeApi"}""" } } diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt index 3cbf80e3..50df3a08 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt @@ -11,12 +11,14 @@ import icu.samnyan.aqua.sega.maimai2.handler.* import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos import icu.samnyan.aqua.spring.Metrics import jakarta.servlet.http.HttpServletRequest +import kotlinx.io.bytestring.hexToByteString import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Lazy import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController +import java.security.MessageDigest import java.time.format.DateTimeFormatter import kotlin.reflect.full.declaredMemberProperties @@ -25,7 +27,11 @@ import kotlin.reflect.full.declaredMemberProperties */ @Suppress("unused") @RestController -@RequestMapping(path = ["/g/mai2/Maimai2Servlet/", "/g/mai2/"]) +@RequestMapping(path = [ + "/g/SDGA/{version}/Maimai2Servlet/", "/g/SDGA/{version}", + "/g/SDEZ/{version}/Maimai2Servlet/", "/g/SDEZ/{version}", + //"/g/SDGB/{version}/Maimai2Servlet/", "/g/SDGB/{version}", +]) class Maimai2ServletController( val upsertUserAll: UpsertUserAllHandler, val getUserItem: GetUserItemHandler, @@ -40,7 +46,18 @@ class Maimai2ServletController( val getGameRanking: GetGameRankingHandler, val db: Mai2Repos, val net: Maimai2, -): MeowApi(serialize = { _, resp -> if (resp is String) resp else resp.toJson() }) { +): MeowApi( + serialize = { _, resp -> if (resp is String) resp else resp.toJson() }, + @kotlin.ExperimentalStdlibApi + { endpoint: String -> + db.gameData.mai2GameEncryption.map{ + val suffix = when (it.code) { "SDGA" -> "MaimaiExp" "SDGB" -> "MaimaiChn" else -> "" } + MessageDigest.getInstance("MD5") + .digest((endpoint + suffix).toByteArray() + it.salt!!.hexToByteArray()) + .toHexString() + } + } +) { companion object { private val log = logger() private val empty = listOf() @@ -55,11 +72,6 @@ class Maimai2ServletController( "CMGetUserCardApi","CMGetUserCardPrintErrorApi","CMGetUserDataApi","CMGetUserItemApi","CMUpsertUserPrintApi", "GetUserFavoriteItemApi") - val noopEndpoint = setOf("GetUserScoreRankingApi", "UpsertClientBookkeepingApi", - "UpsertClientSettingApi", "UpsertClientTestmodeApi", "UpsertClientUploadApi", "Ping", "RemoveTokenApi", - "CMLoginApi", "CMLogoutApi", "CMUpsertBuyCardApi", "UserLogoutApi", "GetGameMapAreaConditionApi", - "UpsertUserChargelogApi","UpsertClientPlayTimeApi") - val members = this::class.declaredMemberProperties val handlers: Map = initH + endpointList.associateWith { api -> val name = api.replace("Api", "").lowercase() @@ -72,27 +84,25 @@ class Maimai2ServletController( @API("/{api}") fun handle(@PathVariable api: String, @RequestBody data: Map, req: HttpServletRequest): Any { val token = TokenChecker.tokenShort() - log.info("$token : $api < ${data.toJson()}") + + val apiFriendlyName = if (api.lowercase() == api && api.length == 32) + ((hashes.filter { it.value.contains(api) }.keys.firstOrNull()?.str + " (encrypt)") ?: api) else "$api (encrypt)" + log.info("$token : $apiFriendlyName < ${data.toJson()}") val noop = """{"returnCode":1,"apiName":"com.sega.maimai2servlet.api.$api"}""" - if (api !in noopEndpoint && !handlers.containsKey(api)) { - log.warn("$token : $api > not found") + if (!handlers.containsKey(api)) { + log.warn("$token : $apiFriendlyName > not found") return noop } // Only record the counter metrics if the API is known. Metrics.counter("aquadx_maimai2_api_call", "api" to api).increment() - if (api in noopEndpoint) { - log.info("$token : $api > no-op") - return noop - } - return try { Metrics.timer("aquadx_maimai2_api_latency", "api" to api).recordCallable { val ctx = RequestContext(req, data.mut) serialize(api, handlers[api]!!(ctx) ?: noop).also { - log.info("$token : $api > ${it.truncate(500)}") + log.info("$token : $apiFriendlyName > ${it.truncate(500)}") } } } catch (e: Exception) { diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt index 7f6fb1f3..b828c7a1 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt @@ -147,7 +147,7 @@ class Mai2Repos( val userKaleidx: MAi2UserKaleidxRepo, val userIntimate: MAi2UserIntimateRepo, val userRegions: Mai2UserRegionsRepo, - gameData: GameDataService + val gameData: GameDataService ) { val gameCharge = StaticRepo(gameData.mai2Charges) { it.orderId } val gameEvent = StaticRepo(gameData.mai2Events) { it.id } diff --git a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiApis.kt b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiApis.kt index 32e477a0..698812d5 100644 --- a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiApis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiApis.kt @@ -70,4 +70,16 @@ fun OngekiController.ongekiInit() { "GetClientTestmode" { empty.staticLst("clientTestmodeList") + mapOf("placeId" to data["placeId"]) } + + // NOTE: no-op APIs moved to respect encryption + "ExtendLockTime" static { """{returnCode":1, "apiName":"ExtendLockTimeApi"}""" } + "GameLogout" static { """{returnCode":1, "apiName":"GameLogoutApi"}""" } + "RegisterPromotionCard" static { """{returnCode":1, "apiName":"RegisterPromotionCardApi"}""" } + "UpsertClientBookkeeping" static { """{returnCode":1, "apiName":"UpsertClientBookkeepingApi"}""" } + "UpsertClientDevelop" static { """{returnCode":1, "apiName":"UpsertClientDevelopApi"}""" } + "UpsertClientError" static { """{returnCode":1, "apiName":"UpsertClientErrorApi"}""" } + "UpsertClientSetting" static { """{returnCode":1, "apiName":"UpsertClientSettingApi"}""" } + "UpsertClientTestmode" static { """{returnCode":1, "apiName":"UpsertClientTestmodeApi"}""" } + "UpsertUserGplog" static { """{returnCode":1, "apiName":"UpsertUserGplogApi"}""" } + "Ping" static { """{returnCode":1, "apiName":"Ping"}""" } } \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiController.kt b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiController.kt index a82bb06c..b8192fe0 100644 --- a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiController.kt +++ b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiController.kt @@ -8,14 +8,18 @@ import icu.samnyan.aqua.sega.general.GameMusicPopularity import icu.samnyan.aqua.sega.general.MeowApi import icu.samnyan.aqua.sega.general.RequestContext import icu.samnyan.aqua.sega.general.service.CardService +import icu.samnyan.aqua.sega.maimai2.Maimai2ServletController import icu.samnyan.aqua.sega.util.BasicMapper import icu.samnyan.aqua.spring.Metrics import jakarta.servlet.http.HttpServletRequest import org.springframework.web.bind.annotation.RestController +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.PBEKeySpec + @Suppress("unused") @RestController -@API("/g/ongeki/{version}", "/g/ongeki") +@API("/g/SDDT/{version}", "/g/SDDT") class OngekiController( val mapper: BasicMapper, val db: OngekiUserRepos, @@ -23,14 +27,23 @@ class OngekiController( val us: AquaUserServices, val pop: GameMusicPopularity, val cardService: CardService, -): MeowApi({ _, resp -> if (resp is String) resp else mapper.write(resp) }) { +): MeowApi( + { _, resp -> if (resp is String) resp else mapper.write(resp) }, + @OptIn(ExperimentalStdlibApi::class) + { endpoint: String -> + gdb.gameData.ogkGameEncryption.map{ + SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") + .generateSecret(PBEKeySpec( + endpoint.toCharArray(), + it.salt!!.hexToByteArray(), + it.iterations, 128 + )).encoded.toHexString().substring(0, 32) + } + } +) { val log = logger() - val noopEndpoint = setOf("ExtendLockTimeApi", "GameLogoutApi", "RegisterPromotionCardApi", - "UpsertClientBookkeepingApi", "UpsertClientDevelopApi", "UpsertClientErrorApi", "UpsertClientSettingApi", - "UpsertClientTestmodeApi", "UpsertUserGplogApi", "Ping") - init { ongekiInit() } val handlers = initH @@ -40,26 +53,25 @@ class OngekiController( version?.let { data["version"] = it } val token = TokenChecker.tokenShort() - log.info("$token : $api < ${data.toJson()}") + + val apiFriendlyName = if (api.lowercase() == api && api.length == 32) + ((hashes.filter { it.value.contains(api) }.keys.firstOrNull()?.str + " (encrypt)") ?: api) else "$api (encrypt)" + log.info("$token : $apiFriendlyName < ${data.toJson()}") val noop = """{"returnCode":1,"apiName":"${api.substringBefore("Api").firstCharLower()}"}""" - if (api !in noopEndpoint && !handlers.containsKey(api)) { - log.warn("$token : $api > not found") + if (!handlers.containsKey(api)) { + log.warn("$token : $apiFriendlyName > not found") return noop } // Only record the counter metrics if the API is known. Metrics.counter("aquadx_ongeki_api_call", "api" to api).increment() - if (api in noopEndpoint) { - log.info("$token : $api > no-op") - return noop - } return try { Metrics.timer("aquadx_ongeki_api_latency", "api" to api).recordCallable { serialize(api, handlers[api]!!(ctx) ?: noop).also { if (api !in setOf("GetUserItemApi", "GetGameEventApi")) - log.info("$token : $api > ${it.truncate(500)}") + log.info("$token : $apiFriendlyName > ${it.truncate(500)}") } } } catch (e: Exception) { diff --git a/src/main/java/icu/samnyan/aqua/sega/util/GameDataService.kt b/src/main/java/icu/samnyan/aqua/sega/util/GameDataService.kt index b2322465..285866d9 100644 --- a/src/main/java/icu/samnyan/aqua/sega/util/GameDataService.kt +++ b/src/main/java/icu/samnyan/aqua/sega/util/GameDataService.kt @@ -3,6 +3,7 @@ package icu.samnyan.aqua.sega.util import com.fasterxml.jackson.core.type.TypeReference import ext.logger import ext.toJson +import icu.samnyan.aqua.sega.general.model.GameEncryptionKey import icu.samnyan.aqua.sega.chusan.model.GameCharge as Chu3GameCharge import icu.samnyan.aqua.sega.chusan.model.GameEvent as Chu3GameEvent import icu.samnyan.aqua.sega.chusan.model.GameGacha as Chu3GameGacha @@ -45,7 +46,8 @@ class GameDataService() { return mapper.read(resStream.bufferedReader().use { it.readText() }, object : TypeReference>() {}) } - log.warn("Game data file $f or resource $resPath not found, using empty list") + if (file != "game_encryption.json") + log.warn("Game data file $f or resource $resPath not found, using empty list") return emptyList() } } @@ -54,6 +56,7 @@ class GameDataService() { lateinit var mai2Events: List lateinit var mai2Charges: List lateinit var mai2SellingCards: List + lateinit var mai2GameEncryption: List // chusan lateinit var chu3GameLinkedVerses: List @@ -63,6 +66,7 @@ class GameDataService() { lateinit var chu3GameGachas: List lateinit var chu3GameLoginBonusPresets: List lateinit var chu3GameLoginBonuses: List + lateinit var chu3GameEncryption: List // ongeki lateinit var ogkGameCards: List @@ -75,6 +79,7 @@ class GameDataService() { lateinit var ogkGameSkills: List lateinit var ogkGameGachaCards: List lateinit var ogkGameGachas: List + lateinit var ogkGameEncryption: List init { load() @@ -84,6 +89,7 @@ class GameDataService() { mai2Events = load("maimai2", "game_event.json") mai2Charges = load("maimai2", "game_charge.json") mai2SellingCards = load("maimai2", "game_selling_card.json") + mai2GameEncryption = load(game = "maimai2", file = "game_encryption.json") chu3GameLinkedVerses = load("chusan", "game_linked_verse.json") chu3GameCharges = load("chusan", "game_charge.json") @@ -92,6 +98,7 @@ class GameDataService() { chu3GameGachas = load("chusan", "game_gacha.json") chu3GameLoginBonusPresets = load("chusan", "game_login_bonus_preset.json") chu3GameLoginBonuses = load("chusan", "game_login_bonus.json") + chu3GameEncryption = load(game = "chusan", file = "game_encryption.json") ogkGameCards = load("ongeki", "game_card.json") ogkGameCharas = load("ongeki", "game_chara.json") @@ -103,6 +110,7 @@ class GameDataService() { ogkGameSkills = load("ongeki", "game_skill.json") ogkGameGachaCards = load("ongeki", "game_gacha_card.json") ogkGameGachas = load("ongeki", "game_gacha.json") + ogkGameEncryption = load(game = "ongeki", file = "game_encryption.json") } } diff --git a/src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt b/src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt index 3c5e28a4..87178ac0 100644 --- a/src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt +++ b/src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt @@ -25,7 +25,7 @@ import kotlin.math.max import kotlin.math.min @RestController -@API("/g/wacca/") +@API("/g/SDFE/{version}") class WaccaServer { // These are lateinit autowired instead of constructor injection because the tests depend on creating // an instance of this class with no arguments.